using System.Text.Json; using Omika.Core.Observability.External; using Omika.Core.Observability.Outbox; using Omika.Diagnostics.Import; using Omika.Diagnostics.Security; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// §54 — Sécurité : aucune donnée interdite dans le modèle persistable. public class DiagnosticSecurityTests { [Fact] public void Mapper_DropsForbiddenMetadataKeysAndValues() { var metadata = new Dictionary { ["password"] = "secret123", // clé interdite ["Authorization"] = "Bearer abc", // clé interdite ["method"] = "Bearer token", // clé autorisée mais valeur interdite ["statusCode"] = 200, ["routeTemplate"] = "/api/test" }; RemoteDiagnosticRecord record = DiagnosticTestData.Record(Guid.NewGuid(), "TEST", metadata: metadata); Models.DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile"); Assert.NotNull(mapped); Assert.NotNull(mapped!.MetadataJson); string json = mapped.MetadataJson!; Assert.DoesNotContain("password", json, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("Authorization", json, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("Bearer", json, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("token", json, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("secret123", json, StringComparison.OrdinalIgnoreCase); var persisted = JsonSerializer.Deserialize>(json); Assert.NotNull(persisted); Assert.False(persisted!.ContainsKey("password")); Assert.False(persisted.ContainsKey("Authorization")); Assert.False(persisted.ContainsKey("method")); // valeur interdite → clé supprimée Assert.True(persisted.ContainsKey("statusCode")); Assert.True(persisted.ContainsKey("routeTemplate")); } [Fact] public void Policy_RejectsForbiddenKeys() { Assert.False(DiagnosticMetadataPolicy.IsAllowedKey("password")); Assert.False(DiagnosticMetadataPolicy.IsAllowedKey("ConnectionString")); Assert.False(DiagnosticMetadataPolicy.IsAllowedKey("RequestBody")); Assert.False(DiagnosticMetadataPolicy.IsAllowedKey("cookie")); Assert.True(DiagnosticMetadataPolicy.IsAllowedKey("method")); Assert.True(DiagnosticMetadataPolicy.IsAllowedKey("statusCode")); } [Fact] public void Mapper_DoesNotPersistBusinessIdentityFields() { // DeviceId/UserId/ClientId/CollaborateurId/EntityId sont volontairement // ABSENTS du modèle persistable (confidentialité, §30/§31). Assert.Null(typeof(Models.DiagnosticEventRecord).GetProperty("DeviceId")); Assert.Null(typeof(Models.DiagnosticEventRecord).GetProperty("UserId")); Assert.Null(typeof(Models.DiagnosticEventRecord).GetProperty("ClientId")); Assert.Null(typeof(Models.DiagnosticEventRecord).GetProperty("CollaborateurId")); Assert.Null(typeof(Models.DiagnosticEventRecord).GetProperty("EntityId")); } [Theory] [InlineData("CollaborateurId")] [InlineData("collaborateur_id")] [InlineData("collaborateur-id")] [InlineData("USER_ID")] [InlineData("deviceId")] [InlineData("client.id")] [InlineData("email")] [InlineData("e-mail")] [InlineData("phone")] [InlineData("telephone")] public void ForbiddenData_RejectsIdentityMetadataKeys(string key) { Assert.True(DiagnosticForbiddenData.IsForbiddenKey(key)); } [Theory] [InlineData("CollaborateurId")] [InlineData("collaborateur_id")] [InlineData("collaborateur-id")] [InlineData("USER_ID")] [InlineData("deviceId")] [InlineData("client.id")] [InlineData("email")] [InlineData("e-mail")] [InlineData("phone")] [InlineData("telephone")] public void MetadataPolicy_RejectsIdentityMetadataKeys(string key) { Assert.False(DiagnosticMetadataPolicy.IsAllowedKey(key)); } [Fact] public void Mapper_DropsIdentityMetadataKeys() { var metadata = new Dictionary { ["CollaborateurId"] = "42", ["UserId"] = "user-1", ["ClientId"] = "BATIK.01", ["DeviceId"] = "device-1", ["email"] = "jean@example.com", ["phone"] = "0123456789" }; RemoteDiagnosticRecord record = DiagnosticTestData.Record(Guid.NewGuid(), "TEST", metadata: metadata); Models.DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile"); Assert.NotNull(mapped); Assert.Null(mapped!.MetadataJson); // toutes les clés d'identité sont rejetées. } [Fact] public void ForbiddenValue_DetectsEmailAndPhoneLiterals() { Assert.True(DiagnosticForbiddenData.ContainsForbiddenValue("email=jean@example.com")); Assert.True(DiagnosticForbiddenData.ContainsForbiddenValue("phone=0123456789")); Assert.False(DiagnosticForbiddenData.ContainsForbiddenValue("composant: télémétrie")); } }