274 lines
9.4 KiB
C#
274 lines
9.4 KiB
C#
using System.Text.Json;
|
|
using Omika.Core.Observability;
|
|
using Omika.Core.Observability.Outbox;
|
|
|
|
namespace Omika.Tests.Observability.Outbox;
|
|
|
|
/// <summary>Tests du mapping événement → DTO transport (§27, §28, §29, §63).</summary>
|
|
public class DiagnosticEventMapperTests
|
|
{
|
|
[Fact]
|
|
public void ToDto_CopiesWhitelistedFields_AndPreservesEventId()
|
|
{
|
|
Guid eventId = Guid.NewGuid();
|
|
Guid traceId = Guid.NewGuid();
|
|
Guid sessionId = Guid.NewGuid();
|
|
Guid operationId = Guid.NewGuid();
|
|
|
|
var e = new TelemetryEvent
|
|
{
|
|
EventId = eventId,
|
|
EventCode = TelemetryEventCodes.MobileAppStarted,
|
|
Level = TelemetryLevel.Error,
|
|
Priority = TelemetryPriority.P1,
|
|
OperationId = operationId,
|
|
Component = "SyncEngine",
|
|
AppVersion = "1.2.3",
|
|
DeviceId = "device-1"
|
|
};
|
|
e.TraceId = traceId;
|
|
e.SessionId = sessionId;
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal(eventId, dto!.EventId);
|
|
Assert.Equal(traceId, dto.TraceId);
|
|
Assert.Equal(sessionId, dto.SessionId);
|
|
Assert.Equal(operationId, dto.OperationId);
|
|
Assert.Equal("device-1", dto.DeviceId);
|
|
Assert.Equal("1.2.3", dto.AppVersion);
|
|
Assert.Equal("SyncEngine", dto.Component);
|
|
Assert.Equal("P1", dto.Priority);
|
|
Assert.Equal("Error", dto.Level);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_CopiesDeviceDimensions_AppBuildAndScreen()
|
|
{
|
|
var e = new TelemetryEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileAppStarted,
|
|
AppBuild = "1410",
|
|
ScreenWidth = 1080,
|
|
ScreenHeight = 2400,
|
|
LogicalWidth = 360,
|
|
LogicalHeight = 800,
|
|
DisplayScale = 3.0
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal("1410", dto!.AppBuild);
|
|
Assert.Equal(1080, dto.ScreenWidth);
|
|
Assert.Equal(2400, dto.ScreenHeight);
|
|
Assert.Equal(360, dto.LogicalWidth);
|
|
Assert.Equal(800, dto.LogicalHeight);
|
|
Assert.Equal(3.0, dto.DisplayScale);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_CopiesScreenCodeAndFeatureCode()
|
|
{
|
|
// T18 : le mapping transport doit conserver ScreenCode/FeatureCode (dimensions
|
|
// déclaratives d'usage) — c'est le contrat vérifié de bout en bout.
|
|
var e = new TelemetryEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileScreenView,
|
|
ScreenCode = DiagnosticScreenCodes.SiteDetail,
|
|
FeatureCode = DiagnosticFeatureCodes.ReportGenerate
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal(DiagnosticScreenCodes.SiteDetail, dto!.ScreenCode);
|
|
Assert.Equal(DiagnosticFeatureCodes.ReportGenerate, dto.FeatureCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundTrip_PreservesScreenCodeAndFeatureCode()
|
|
{
|
|
var e = new TelemetryEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileScreenView,
|
|
ScreenCode = DiagnosticScreenCodes.RendezVousDetail,
|
|
FeatureCode = DiagnosticFeatureCodes.InterventionSearchRun
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
string json = DiagnosticEventMapper.Serialize(dto!);
|
|
DiagnosticEventDto? back = DiagnosticEventMapper.Deserialize(json);
|
|
|
|
Assert.Equal(DiagnosticScreenCodes.RendezVousDetail, back!.ScreenCode);
|
|
Assert.Equal(DiagnosticFeatureCodes.InterventionSearchRun, back.FeatureCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_NullScreenCodeAndFeatureCode_StayNull()
|
|
{
|
|
// Compatibilité : les événements historiques/background sans usage fonctionnel
|
|
// restent ScreenCode=null / FeatureCode=null (aucune invention).
|
|
var e = new TelemetryEvent { EventCode = TelemetryEventCodes.MobileAppStarted };
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Null(dto!.ScreenCode);
|
|
Assert.Null(dto.FeatureCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_InvalidEventCode_ReturnsNull()
|
|
{
|
|
TelemetryEvent e = OutboxTestHarness.NewEvent(code: EventCode.Invalid);
|
|
|
|
Assert.Null(DiagnosticEventMapper.ToDto(e, 256));
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_DropsForbiddenMetadataKeys()
|
|
{
|
|
TelemetryEvent e = OutboxTestHarness.NewEvent(metadata: TelemetryMetadata.Create(
|
|
("password", "secret"),
|
|
("method", "POST"),
|
|
("token", "abc")));
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.NotNull(dto!.Metadata);
|
|
Assert.True(dto.Metadata!.ContainsKey("method"));
|
|
Assert.False(dto.Metadata.ContainsKey("password"));
|
|
Assert.False(dto.Metadata.ContainsKey("token"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_DropsForbiddenMetadataValues()
|
|
{
|
|
TelemetryEvent e = OutboxTestHarness.NewEvent(metadata: TelemetryMetadata.Create(
|
|
("error", "ConnectionString=Server=prod;Password=hidden"),
|
|
("ok", "échec")));
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.NotNull(dto!.Metadata);
|
|
Assert.False(dto.Metadata.ContainsKey("error"));
|
|
Assert.True(dto.Metadata.ContainsKey("ok"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_TruncatesLongStrings()
|
|
{
|
|
var e = new TelemetryEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileAppStarted,
|
|
Component = new string('x', 1000)
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal(256, dto!.Component!.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundTrip_PreservesUtf8Accents()
|
|
{
|
|
var e = new TelemetryEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileAppStarted,
|
|
Component = "télémétrie",
|
|
EntityType = "Créé"
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
string json = DiagnosticEventMapper.Serialize(dto!);
|
|
DiagnosticEventDto? back = DiagnosticEventMapper.Deserialize(json);
|
|
|
|
Assert.Equal("télémétrie", back!.Component);
|
|
Assert.Equal("Créé", back.EntityType);
|
|
}
|
|
|
|
[Fact]
|
|
public void Deserialize_UnknownFields_AreDropped()
|
|
{
|
|
// Un champ "password" non prévu dans le DTO est ignoré par la désérialisation
|
|
// typée (aucun JsonExtensionData) : il ne sera jamais persisté. (§60)
|
|
string json = """
|
|
{ "eventId": "00000000-0000-0000-0000-000000000001", "eventCode": "OMK-MOB-APP-001",
|
|
"password": "secret", "component": "test" }
|
|
""";
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.Deserialize(json);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal("test", dto!.Component);
|
|
Assert.Null(dto.Metadata);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_CopiesExceptionDetail()
|
|
{
|
|
// Phase 6 : message/type/stack trace d'exception sont des champs first-class
|
|
// du transport (jamais des métadonnées libres), préservés de bout en bout.
|
|
var e = new DiagnosticEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileHandledException,
|
|
Message = "Échec d'écriture",
|
|
ExceptionType = "System.IO.IOException",
|
|
StackTrace = "at Omika.App.Services.SyncEngine.Run()"
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal("Échec d'écriture", dto!.Message);
|
|
Assert.Equal("System.IO.IOException", dto.ExceptionType);
|
|
Assert.Equal("at Omika.App.Services.SyncEngine.Run()", dto.StackTrace);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_TruncatesExceptionDetail()
|
|
{
|
|
var e = new DiagnosticEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileUnhandledException,
|
|
Message = new string('m', 500),
|
|
ExceptionType = new string('t', 500),
|
|
StackTrace = new string('s', 3000)
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.Equal(256, dto!.Message!.Length);
|
|
Assert.Equal(256, dto.ExceptionType!.Length);
|
|
Assert.Equal(DiagnosticEventMapper.MaxStackTraceLength, dto.StackTrace!.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_SignaleLaTroncatureParUnMarqueur()
|
|
{
|
|
// Une coupe SILENCIEUSE se lit comme une valeur complète : sur un vrai ticket,
|
|
// une pile coupée en plein identifiant laissait croire qu'elle s'arrêtait là.
|
|
var e = new DiagnosticEvent
|
|
{
|
|
EventCode = TelemetryEventCodes.MobileUnhandledException,
|
|
Message = new string('m', 500),
|
|
StackTrace = new string('s', 3000)
|
|
};
|
|
|
|
DiagnosticEventDto? dto = DiagnosticEventMapper.ToDto(e, 256);
|
|
|
|
Assert.NotNull(dto);
|
|
Assert.EndsWith("…[tronqué]", dto!.Message, StringComparison.Ordinal);
|
|
Assert.EndsWith("…[tronqué]", dto.StackTrace, StringComparison.Ordinal);
|
|
// Le marqueur tient DANS la limite : la valeur transmise reste bornée.
|
|
Assert.Equal(256, dto.Message!.Length);
|
|
Assert.Equal(DiagnosticEventMapper.MaxStackTraceLength, dto.StackTrace!.Length);
|
|
}
|
|
}
|