first commit

This commit is contained in:
2026-09-10 16:43:09 +02:00
commit 79cf35f690
144 changed files with 8822 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
using System.Text.Json;
using App.Core.Interfaces.Licensing;
using App.Core.Services.Licensing;
namespace App.Core.Tests.Licensing;
/// <summary>
/// Couvre uniquement le flux "essai" de LicenseService (pas de licence installée) : c'est la
/// partie testable sans dépendre de la vraie clé publique Ed25519, qui n'est pas encore générée
/// (voir Ed25519PublicKeyProvider). Le flux "licence installée" (ActivateLicense / licence déjà
/// présente) sera couvert une fois LicenseGenerator exécuté pour de vrai et la clé embarquée.
/// </summary>
public class LicenseServiceTests
{
private sealed class FakeHardwareIdProvider(HardwareId hardwareId) : IHardwareIdProvider
{
public HardwareId GetCurrent() => hardwareId;
}
private sealed class FakeTrialStateStore : ITrialStateStore
{
public TrialState? Stored { get; private set; }
public TrialState? Load() => Stored;
public void Save(TrialState state) => Stored = state;
}
private sealed class FakeLicenseFileRepository : ILicenseFileRepository
{
public LicenseFile? Installed { get; set; }
public LicenseFile? LoadInstalled() => Installed;
public void Install(LicenseFile license) => Installed = license;
}
private sealed class CorruptedLicenseFileRepository : ILicenseFileRepository
{
public LicenseFile? LoadInstalled() => throw new JsonException("fichier .lic corrompu (test)");
public void Install(LicenseFile license) => throw new NotSupportedException();
}
private static readonly HardwareId TestHardware = new("MB-TEST", "DISK-TEST");
private static LicenseService CreateService(FakeTrialStateStore trialStore, ILicenseFileRepository? licenseRepo = null) =>
new(new FakeHardwareIdProvider(TestHardware), trialStore, licenseRepo ?? new FakeLicenseFileRepository());
[Fact]
public void CheckLicenseStatus_FirstLaunch_CreatesTrialAndReturnsFullDuration()
{
var trialStore = new FakeTrialStateStore();
var service = CreateService(trialStore);
var now = new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Utc);
var result = service.CheckLicenseStatus(now);
Assert.Equal(LicenseStatus.TrialActive, result.Status);
Assert.Equal(TrialState.TrialDurationDays, result.TrialDaysRemaining);
Assert.NotNull(trialStore.Stored);
Assert.Equal(now, trialStore.Stored!.InstallDateUtc);
Assert.Equal(TestHardware.ComputeHash(), trialStore.Stored.HardwareIdHash);
}
[Fact]
public void CheckLicenseStatus_SubsequentLaunch_ReturnsRemainingDaysAndUpdatesLastRun()
{
var trialStore = new FakeTrialStateStore();
var service = CreateService(trialStore);
var installDate = new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Utc);
service.CheckLicenseStatus(installDate);
var result = service.CheckLicenseStatus(installDate.AddDays(10));
Assert.Equal(LicenseStatus.TrialActive, result.Status);
Assert.Equal(20, result.TrialDaysRemaining);
Assert.Equal(installDate.AddDays(10), trialStore.Stored!.LastRunTimestampUtc);
Assert.Equal(installDate, trialStore.Stored.InstallDateUtc); // date d'installation jamais modifiée
}
[Fact]
public void CheckLicenseStatus_After30Days_ReturnsTrialExpired()
{
var trialStore = new FakeTrialStateStore();
var service = CreateService(trialStore);
var installDate = new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Utc);
service.CheckLicenseStatus(installDate);
var result = service.CheckLicenseStatus(installDate.AddDays(31));
Assert.Equal(LicenseStatus.TrialExpired, result.Status);
Assert.Equal(0, result.TrialDaysRemaining);
}
[Fact]
public void CheckLicenseStatus_ClockRolledBack_ReturnsClockTamperDetected()
{
var trialStore = new FakeTrialStateStore();
var service = CreateService(trialStore);
var installDate = new DateTime(2026, 1, 15, 9, 0, 0, DateTimeKind.Utc);
service.CheckLicenseStatus(installDate);
// L'utilisateur recule l'horloge système de 5 jours pour tenter de prolonger l'essai.
var result = service.CheckLicenseStatus(installDate.AddDays(-5));
Assert.Equal(LicenseStatus.ClockTamperDetected, result.Status);
}
[Fact]
public void GetCurrentHardwareId_DelegatesToHardwareIdProvider()
{
var service = CreateService(new FakeTrialStateStore());
Assert.Equal(TestHardware, service.GetCurrentHardwareId());
}
[Fact]
public void CheckLicenseStatus_CorruptedLicenseFile_ReturnsInvalidRatherThanFallingBackToTrial()
{
var trialStore = new FakeTrialStateStore();
var service = CreateService(trialStore, new CorruptedLicenseFileRepository());
var result = service.CheckLicenseStatus(DateTime.UtcNow);
Assert.Equal(LicenseStatus.InvalidSignature, result.Status);
// Ne doit surtout pas avoir créé un nouvel essai : corrompre le fichier ne doit jamais
// redonner 30 jours gratuits.
Assert.Null(trialStore.Stored);
}
}

View File

@@ -0,0 +1,89 @@
using App.Core.Services.Licensing;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Security;
namespace App.Core.Tests.Licensing;
public class LicenseValidatorTests
{
/// <summary>
/// Génère une paire de clés de TEST (jamais la clé de production) et signe une licence
/// avec, exactement comme le fait LicenseGenerator. Permet de tester LicenseValidator sans
/// dépendre de la clé publique embarquée dans Ed25519PublicKeyProvider.
/// </summary>
private static (byte[] PublicKey, LicenseFile SignedLicense) CreateSignedLicense(
string companyName = "ACME Industries",
string motherboardUuid = "MB-1234",
string diskSerial = "DISK-5678",
int maxUsers = 5,
int maxMachines = 50)
{
var generator = new Ed25519KeyPairGenerator();
generator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
var keyPair = generator.GenerateKeyPair();
var privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
var publicKey = (Ed25519PublicKeyParameters)keyPair.Public;
var unsigned = new LicenseFile(
companyName, motherboardUuid, diskSerial,
new DateOnly(2026, 1, 1), null, maxUsers, maxMachines, Signature: string.Empty);
var canonicalBytes = LicenseFileCanonicalSerializer.GetCanonicalBytes(unsigned);
var signer = new Ed25519Signer();
signer.Init(forSigning: true, privateKey);
signer.BlockUpdate(canonicalBytes, 0, canonicalBytes.Length);
var signature = Convert.ToBase64String(signer.GenerateSignature());
return (publicKey.GetEncoded(), unsigned with { Signature = signature });
}
[Fact]
public void VerifySignature_ValidLicense_ReturnsTrue()
{
var (publicKey, license) = CreateSignedLicense();
Assert.True(LicenseValidator.VerifySignature(license, publicKey));
}
[Fact]
public void VerifySignature_TamperedField_ReturnsFalse()
{
var (publicKey, license) = CreateSignedLicense();
var tampered = license with { MaxUsers = 9999 };
Assert.False(LicenseValidator.VerifySignature(tampered, publicKey));
}
[Fact]
public void VerifySignature_WrongPublicKey_ReturnsFalse()
{
var (_, license) = CreateSignedLicense();
var (otherPublicKey, _) = CreateSignedLicense();
Assert.False(LicenseValidator.VerifySignature(license, otherPublicKey));
}
[Fact]
public void VerifySignature_CorruptedBase64Signature_ReturnsFalseWithoutThrowing()
{
var (publicKey, license) = CreateSignedLicense();
var corrupted = license with { Signature = "pas-du-base64-valide!!" };
Assert.False(LicenseValidator.VerifySignature(corrupted, publicKey));
}
[Theory]
[InlineData("MB-1234", "DISK-5678", true)]
[InlineData("mb-1234", "disk-5678", true)] // comparaison insensible à la casse
[InlineData("MB-9999", "DISK-5678", false)]
[InlineData("MB-1234", "DISK-0000", false)]
public void MatchesHardware_RequiresBothIdentifiersToMatch(string motherboard, string disk, bool expected)
{
var (_, license) = CreateSignedLicense(motherboardUuid: "MB-1234", diskSerial: "DISK-5678");
var current = new HardwareId(motherboard, disk);
Assert.Equal(expected, LicenseValidator.MatchesHardware(license, current));
}
}

View File

@@ -0,0 +1,55 @@
using App.Core.Services.Licensing;
namespace App.Core.Tests.Licensing;
public class TrialStateTests
{
[Fact]
public void GetDaysRemaining_OnInstallDay_Returns30()
{
var installDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var state = new TrialState(installDate, "hash", installDate);
Assert.Equal(30, state.GetDaysRemaining(installDate));
}
[Fact]
public void GetDaysRemaining_AfterFifteenDays_Returns15()
{
var installDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var state = new TrialState(installDate, "hash", installDate);
Assert.Equal(15, state.GetDaysRemaining(installDate.AddDays(15)));
}
[Fact]
public void GetDaysRemaining_NeverGoesNegative()
{
var installDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var state = new TrialState(installDate, "hash", installDate);
Assert.Equal(0, state.GetDaysRemaining(installDate.AddDays(90)));
}
[Fact]
public void IsExpired_ExactlyOnDay30_IsTrue()
{
var installDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var state = new TrialState(installDate, "hash", installDate);
Assert.True(state.IsExpired(installDate.AddDays(30)));
Assert.False(state.IsExpired(installDate.AddDays(29)));
}
[Fact]
public void WithUpdatedLastRun_DoesNotChangeInstallDate()
{
var installDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var state = new TrialState(installDate, "hash", installDate);
var updated = state.WithUpdatedLastRun(installDate.AddDays(5));
Assert.Equal(installDate, updated.InstallDateUtc);
Assert.Equal(installDate.AddDays(5), updated.LastRunTimestampUtc);
}
}