using Omika.Core.Observability.Identity; namespace Omika.Diagnostics.Tests; /// /// Tests unitaires de la DeviceKey (T17-B) : HMAC-SHA256 serveur, stable par /// (tenant, device), non réversible, tenant-scoped, séparation de domaine avec /// l'ActorKey, fail-open. Aucune clé de production utilisée ici (clés de test /// déterministes uniquement). /// public class DiagnosticDeviceKeyTests { private static string KeyA() => Convert.ToBase64String(new byte[32]); // 32 octets 0x00 private static string KeyB() => Convert.ToBase64String(Enumerable.Repeat((byte)7, 32).ToArray()); [Fact] public void SameTenant_SameDevice_ProducesSameKey() { var provider = new DiagnosticDeviceKeyProvider(KeyA()); DiagnosticDeviceKey a = provider.Compute("BATIK.01", "device-1"); DiagnosticDeviceKey b = provider.Compute("BATIK.01", "device-1"); Assert.True(a.HasValue); Assert.Equal(a.Value, b.Value); Assert.Equal(a.Version, b.Version); } [Fact] public void SameTenant_DifferentDevice_ProducesDifferentKey() { var provider = new DiagnosticDeviceKeyProvider(KeyA()); DiagnosticDeviceKey a = provider.Compute("BATIK.01", "device-1"); DiagnosticDeviceKey b = provider.Compute("BATIK.01", "device-2"); Assert.NotEqual(a.Value, b.Value); } [Fact] public void DifferentTenant_SameDevice_ProducesDifferentKey() { var provider = new DiagnosticDeviceKeyProvider(KeyA()); DiagnosticDeviceKey a = provider.Compute("BATIK.01", "device-1"); DiagnosticDeviceKey b = provider.Compute("BATIK.02", "device-1"); Assert.NotEqual(a.Value, b.Value); } [Fact] public void DifferentKey_ProducesDifferentDeviceKey() { DiagnosticDeviceKey a = new DiagnosticDeviceKeyProvider(KeyA()).Compute("BATIK.01", "device-1"); DiagnosticDeviceKey b = new DiagnosticDeviceKeyProvider(KeyB()).Compute("BATIK.01", "device-1"); Assert.True(a.HasValue); Assert.True(b.HasValue); Assert.NotEqual(a.Value, b.Value); } [Fact] public void DeviceKey_IsHexOnly_AndContainsNoIdentity() { DiagnosticDeviceKey key = new DiagnosticDeviceKeyProvider(KeyA()).Compute("BATIK.01", "device-12345"); Assert.NotNull(key.Value); Assert.Equal(64, key.Value!.Length); Assert.Matches("^[0-9a-f]{64}$", key.Value); Assert.DoesNotContain("device", key.Value, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("12345", key.Value, StringComparison.Ordinal); Assert.DoesNotContain("BATIK", key.Value, StringComparison.OrdinalIgnoreCase); // La clé HMAC elle-même ne doit jamais fuiter dans le résultat. Assert.DoesNotContain(KeyA(), key.Value, StringComparison.OrdinalIgnoreCase); } [Fact] public void DeviceKey_HasStableFormat_Versioned() { DiagnosticDeviceKey key = new DiagnosticDeviceKeyProvider(KeyA()).Compute("BATIK.01", "device-7"); Assert.Equal(1, key.Version); Assert.NotNull(key.Value); Assert.Equal(64, key.Value!.Length); Assert.Equal(key.Value.ToLowerInvariant(), key.Value); // hex minuscule } [Fact] public void MissingKey_IsFailOpen_ReturnsEmpty() { var cases = new[] { new DiagnosticDeviceKeyProvider(null), new DiagnosticDeviceKeyProvider(string.Empty), new DiagnosticDeviceKeyProvider(" "), new DiagnosticDeviceKeyProvider("not-base64!!!"), new DiagnosticDeviceKeyProvider(Convert.ToBase64String(new byte[16])) // 128 bits < 256 }; foreach (DiagnosticDeviceKeyProvider provider in cases) { DiagnosticDeviceKey key = provider.Compute("BATIK.01", "device-1"); Assert.False(key.HasValue); Assert.Null(key.Value); Assert.Null(key.Version); } } [Fact] public void MissingIdentity_IsFailOpen_ReturnsEmpty() { var provider = new DiagnosticDeviceKeyProvider(KeyA()); Assert.False(provider.Compute("", "device-1").HasValue); Assert.False(provider.Compute(" ", "device-1").HasValue); Assert.False(provider.Compute("BATIK.01", null).HasValue); Assert.False(provider.Compute("BATIK.01", "").HasValue); Assert.False(provider.Compute("BATIK.01", " ").HasValue); } [Fact] public void CanonicalString_IsVersionedAndDeterministic() { string a = DiagnosticDeviceKeyProvider.BuildCanonicalString(1, "BATIK.01", "device-1"); string b = DiagnosticDeviceKeyProvider.BuildCanonicalString(1, "BATIK.01", "device-1"); Assert.Equal(a, b); Assert.Equal("v1|tenant:BATIK.01|device:device-1", a); } [Fact] public void DeviceKey_IsDomainSeparated_FromActorKey() { // Même clé HMAC, même tenant, même valeur d'identifiant : les deux pseudonymes // doivent rester distincts grâce au libellé de champ (device: vs collaborateur:). var actor = new DiagnosticActorKeyProvider(KeyA()); var device = new DiagnosticDeviceKeyProvider(KeyA()); DiagnosticActorKey actorKey = actor.Compute("BATIK.01", "42"); DiagnosticDeviceKey deviceKey = device.Compute("BATIK.01", "42"); Assert.True(actorKey.HasValue); Assert.True(deviceKey.HasValue); Assert.NotEqual(actorKey.Value, deviceKey.Value); } }