using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Omika.Core.Contracts.Mobile.Enrollment;
using Omika.Core.Services.Mobile;
namespace Omika.Tests.Mobile.Local;
///
/// Tests du codec de QR d'inscription (signature RSA-SHA256 + décodage/vérification
/// par kid). Utilise une paire de clés éphémère : aucun secret serveur embarqué.
///
public class EnrollmentQrCodecTests
{
private const string Kid = "test-kid-1";
private readonly string _publicKey;
private readonly string _privateKey;
private readonly IReadOnlyDictionary _registry;
public EnrollmentQrCodecTests()
{
using var rsa = RSA.Create(2048);
_publicKey = rsa.ToXmlString(false);
_privateKey = rsa.ToXmlString(true);
_registry = new Dictionary { [Kid] = _publicKey };
}
private string Sign(string code, string numero, string url)
=> EnrollmentQrCodec.Sign(
EnrollmentQrCodec.BuildPayloadJson(Kid, code, numero, url), _privateKey);
/// Signe un payload JSON arbitraire (pour forger des cas invalides).
private string SignJson(string json)
=> EnrollmentQrCodec.Sign(json, _privateKey);
private string Envelope(string payloadB64Url, string signatureB64Url)
=> EnrollmentQrCodec.Prefix + payloadB64Url + "." + signatureB64Url;
private static string B64Url(string raw)
=> Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)).TrimEnd('=').Replace('+', '-').Replace('/', '_');
// ===== Cas acceptés =====
[Fact]
public void QrSigne_Accepte_CodeClient()
{
EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(Sign("BATIK.01", "LIC-001", "https://licence.example.com"), _registry);
Assert.NotNull(p);
Assert.Equal("BATIK.01", p.CodeClient);
}
[Fact]
public void QrSigne_Accepte_NumeroLicence()
{
EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(Sign("BATIK.01", "LIC-001", "https://licence.example.com"), _registry);
Assert.NotNull(p);
Assert.Equal("LIC-001", p.NumeroLicence);
}
[Fact]
public void QrSigne_Accepte_UrlLicence()
{
EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(Sign("BATIK.01", "LIC-001", "https://licence.example.com"), _registry);
Assert.NotNull(p);
Assert.Equal("https://licence.example.com", p.UrlLicence);
}
// ===== Cas refusés =====
[Fact]
public void PayloadModifie_Refuse()
{
string qr = Sign("BATIK.01", "LIC-001", "https://licence.example.com");
// Altération du contenu signé.
Assert.Null(EnrollmentQrCodec.Decode(qr + "x", _registry));
}
[Fact]
public void SignatureModifiee_Refuse()
{
string qr = Sign("BATIK.01", "LIC-001", "https://licence.example.com");
int dot = qr.LastIndexOf('.');
string tampered = qr[..(dot + 1)] + "AAAA"; // signature corrompue
Assert.Null(EnrollmentQrCodec.Decode(tampered, _registry));
}
[Fact]
public void MauvaiseCle_Refuse()
{
using var other = RSA.Create(2048);
string qr = EnrollmentQrCodec.Sign(
EnrollmentQrCodec.BuildPayloadJson(Kid, "BATIK.01", "LIC-001", "https://licence.example.com"),
other.ToXmlString(true));
Assert.Null(EnrollmentQrCodec.Decode(qr, _registry));
}
[Fact]
public void Base64Invalide_Refuse()
{
Assert.Null(EnrollmentQrCodec.Decode(Envelope("!!!", "!!!"), _registry));
}
[Fact]
public void JsonInvalide_Refuse()
{
string qr = SignJson("{not-json");
Assert.Null(EnrollmentQrCodec.Decode(qr, _registry));
}
[Fact]
public void VersionInconnue_Refuse()
{
string json = JsonSerializer.Serialize(new
{
version = 99,
keyId = Kid,
codeClient = "BATIK.01",
numeroLicence = "LIC-001",
urlLicence = "https://licence.example.com"
});
Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry));
}
[Fact]
public void CodeClientVide_Refuse()
{
string json = JsonSerializer.Serialize(new
{
version = 1,
keyId = Kid,
codeClient = "",
numeroLicence = "LIC-001",
urlLicence = "https://licence.example.com"
});
Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry));
}
[Fact]
public void NumeroLicenceVide_Refuse()
{
string json = JsonSerializer.Serialize(new
{
version = 1,
keyId = Kid,
codeClient = "BATIK.01",
numeroLicence = "",
urlLicence = "https://licence.example.com"
});
Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry));
}
[Fact]
public void UrlLicenceVide_Refuse()
{
string json = JsonSerializer.Serialize(new
{
version = 1,
keyId = Kid,
codeClient = "BATIK.01",
numeroLicence = "LIC-001",
urlLicence = ""
});
Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry));
}
[Fact]
public void KidInconnu_Refuse()
{
using var other = RSA.Create(2048);
string qr = EnrollmentQrCodec.Sign(
EnrollmentQrCodec.BuildPayloadJson("autre-kid", "BATIK.01", "LIC-001", "https://licence.example.com"),
other.ToXmlString(true));
Assert.Null(EnrollmentQrCodec.Decode(qr, _registry));
}
[Fact]
public void RotationKid_DeuxCles_VerifieAvecBonneCle()
{
// Simule une rotation : le registre contient l'ancienne ET la nouvelle clé.
using var oldRsa = RSA.Create(2048);
using var newRsa = RSA.Create(2048);
var registry = new Dictionary
{
["kid-old"] = oldRsa.ToXmlString(false),
["kid-new"] = newRsa.ToXmlString(false)
};
string qrNew = EnrollmentQrCodec.Sign(
EnrollmentQrCodec.BuildPayloadJson("kid-new", "BATIK.01", "LIC-001", "https://licence.example.com"),
newRsa.ToXmlString(true));
EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(qrNew, registry);
Assert.NotNull(p);
Assert.Equal("BATIK.01", p.CodeClient);
}
}