Files
Omika/tests/Omika.Tests/Mobile/Local/DevelopmentLicenseTests.cs
T
2026-10-23 18:46:50 +02:00

347 lines
14 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Omika.App.Local.Abstractions;
using Omika.App.Local.Services;
using Omika.Core.Configuration;
using Omika.Core.Contracts.Mobile.License;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests du mécanisme de licence de développement locale (MICRO-LOT).
///
/// Règles vérifiées :
/// 1. Development + licence dev activée → licence acceptée localement.
/// 2. Development + licence dev désactivée → comportement normal de licence.
/// 3. Production/Release → la licence de développement ne peut jamais être
/// utilisée (le bypass est compilé hors sous #if DEBUG).
/// 4. Aucune requête au serveur de licence lorsque le mode dev est actif.
/// </summary>
public class DevelopmentLicenseTests : LocalTestBase
{
private static DevelopmentLicenseOptions EnabledOptions() => new()
{
Enabled = true,
ClientCode = "LOCAL-DEV",
UserDisplayName = "Utilisateur test",
ExpirationDays = 365
};
private static DevelopmentLicenseOptions DisabledOptions() => new()
{
Enabled = false,
ClientCode = "LOCAL-DEV",
UserDisplayName = "Utilisateur test",
ExpirationDays = 365
};
private static DevelopmentLicenseProvider CreateProvider(DevelopmentLicenseOptions options)
=> new(options, NullLogger<DevelopmentLicenseProvider>.Instance);
// ------------------------------------------------------------------
// 1. Development + licence dev activée → licence acceptée localement.
// ------------------------------------------------------------------
[Fact]
public async Task Provider_DevActive_RetourneLicenceLocaleValide()
{
// Arrange
var provider = CreateProvider(EnabledOptions());
// Act
LicenseResponse? license = await provider.TryGetDevelopmentLicenseAsync();
// Assert
Assert.NotNull(license);
Assert.True(license.IsValid);
Assert.Equal("LOCAL-DEV", license.CodeClient);
Assert.Equal("Utilisateur test", license.UserNom);
Assert.Equal(LicenseErrorCodes.Ok, license.ErrorCode);
Assert.NotNull(license.DateExpiration);
Assert.True(license.DateExpiration > DateTime.Now);
}
[Fact]
public async Task Provider_DevActive_ExpirationSuffisammentLongue()
{
// Arrange
var provider = CreateProvider(EnabledOptions());
// Act
LicenseResponse? license = await provider.TryGetDevelopmentLicenseAsync();
// Assert : expiration longue pour le test local (365 jours).
Assert.NotNull(license);
Assert.NotNull(license.DateExpiration);
Assert.True(license.DateExpiration > DateTime.Now.AddDays(300));
}
[Fact]
public async Task SessionService_DevActive_ActivationLocale_SansAppelServeur()
{
// Arrange : provider dev actif + API qui échouerait si appelée.
var provider = CreateProvider(EnabledOptions());
var api = new ThrowingApiClient();
var service = CreateSessionService(api, provider);
// Act : activation locale.
LicenseResponse result = await service.ActivateAsync("LOCAL-DEV", "DEV-LOCAL-LICENSE");
// Assert : licence locale valide, AUCUN appel serveur (l'API lèverait sinon).
Assert.True(result.IsValid);
Assert.Equal("LOCAL-DEV", result.CodeClient);
Assert.Equal(0, api.ActivateCalls);
}
[Fact]
public async Task SessionService_DevActive_HasValidSession_Vrai()
{
// Arrange
var provider = CreateProvider(EnabledOptions());
var api = new ThrowingApiClient();
var service = CreateSessionService(api, provider);
// Act
bool hasValid = await service.HasValidSessionAsync();
// Assert : session considérée valide localement (accès direct au Shell).
Assert.True(hasValid);
}
// ------------------------------------------------------------------
// 2. Development + licence dev désactivée → comportement normal.
// ------------------------------------------------------------------
[Fact]
public async Task Provider_DevDesactive_RetourneNull()
{
// Arrange
var provider = CreateProvider(DisabledOptions());
// Act
LicenseResponse? license = await provider.TryGetDevelopmentLicenseAsync();
// Assert : aucune licence locale.
Assert.Null(license);
}
[Fact]
public async Task SessionService_DevDesactive_ActivationAppelleServeur()
{
// Arrange : provider dev désactivé + API qui enregistre les appels.
var provider = CreateProvider(DisabledOptions());
var api = new RecordingApiClient();
var service = CreateSessionService(api, provider);
// Act : activation normale.
LicenseResponse result = await service.ActivateAsync("CLIENT01", "LIC-001");
// Assert : le serveur est appelé (comportement normal).
Assert.Equal(1, api.ActivateCalls);
Assert.NotNull(api.LastActivateRequest);
Assert.Equal("CLIENT01", api.LastActivateRequest.CodeClient);
Assert.Equal("LIC-001", api.LastActivateRequest.NumeroLicence);
}
[Fact]
public async Task SessionService_DevDesactive_HasValidSession_Faux()
{
// Arrange : aucune session locale.
var provider = CreateProvider(DisabledOptions());
var api = new RecordingApiClient();
var service = CreateSessionService(api, provider);
// Act
bool hasValid = await service.HasValidSessionAsync();
// Assert : pas de session valide → écran d'activation.
Assert.False(hasValid);
}
// ------------------------------------------------------------------
// 3. Production/Release → la licence de développement ne peut jamais
// être utilisée.
// ------------------------------------------------------------------
[Fact]
public async Task Provider_DevDesactive_ComportementRelease()
{
// En Release, le bloc #if DEBUG est compilé hors : le provider retourne
// toujours null, exactement comme ici avec Enabled=false. Ce test
// documente le contrat de sécurité : aucune licence locale en production.
var provider = CreateProvider(DisabledOptions());
LicenseResponse? license = await provider.TryGetDevelopmentLicenseAsync();
Assert.Null(license);
}
[Fact]
public async Task SessionService_SansProvider_ComportementNormal()
{
// En Release, AppServices ne fournit PAS de provider (compilé hors).
// Le service doit donc se comporter normalement : appel serveur.
var api = new RecordingApiClient();
var service = CreateSessionService(api, developmentLicenseProvider: null);
LicenseResponse result = await service.ActivateAsync("CLIENT01", "LIC-001");
Assert.Equal(1, api.ActivateCalls);
Assert.NotNull(api.LastActivateRequest);
}
// ------------------------------------------------------------------
// 4. Aucune requête au serveur de licence lorsque le mode dev est actif.
// ------------------------------------------------------------------
[Fact]
public async Task SessionService_DevActive_AucunAppelServeur()
{
// Arrange : provider dev actif + API qui lève si appelée.
var provider = CreateProvider(EnabledOptions());
var api = new ThrowingApiClient();
var service = CreateSessionService(api, provider);
// Act : activation locale.
LicenseResponse result = await service.ActivateAsync("LOCAL-DEV", "DEV-LOCAL-LICENSE");
// Assert : aucun appel serveur (ThrowingApiClient lèverait sinon).
Assert.True(result.IsValid);
Assert.Equal(0, api.ActivateCalls);
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
private MobileSessionService CreateSessionService(
IMobileApiClient api,
IDevelopmentLicenseProvider? developmentLicenseProvider)
=> new(
api,
new FakeTokenStore(),
new FakeDeviceIdProvider(),
BootstrapRepository,
NullLogger<MobileSessionService>.Instance,
developmentLicenseProvider);
private sealed class FakeTokenStore : ISecureTokenStore
{
public string? Token { get; private set; }
public Task SaveAsync(string token, CancellationToken ct = default)
{
Token = token;
return Task.CompletedTask;
}
public Task<string?> GetAsync(CancellationToken ct = default) => Task.FromResult(Token);
public Task ClearAsync(CancellationToken ct = default)
{
Token = null;
return Task.CompletedTask;
}
}
private sealed class FakeDeviceIdProvider : IDeviceIdProvider
{
public Task<string> GetDeviceIdAsync(CancellationToken ct = default) => Task.FromResult("device-dev-1");
}
/// <summary>
/// Client API qui lève une exception si appelé : prouve qu'aucune requête
/// serveur n'est émise en mode dev actif.
/// </summary>
private sealed class ThrowingApiClient : IMobileApiClient
{
public string? AccessToken { get; set; }
public int ActivateCalls { get; private set; }
public Task<Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse> ActivateAsync(
LicenseActivateRequest request, CancellationToken ct = default)
{
ActivateCalls++;
throw new InvalidOperationException("Le serveur de licence ne doit PAS être appelé en mode dev.");
}
public Task<Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse> ValidateAsync(
LicenseValidateRequest request, CancellationToken ct = default)
=> throw new InvalidOperationException("Le serveur de licence ne doit PAS être appelé en mode dev.");
public Task<Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse> GetDevelopmentSessionAsync(string deviceId, CancellationToken ct = default)
=> throw new InvalidOperationException("Le serveur ne doit PAS être appelé en mode dev.");
public Task<Omika.Core.Contracts.Mobile.Bootstrap.MobileBootstrapResponse> GetBootstrapAsync(CancellationToken ct = default)
=> throw new InvalidOperationException("Le serveur ne doit PAS être appelé en mode dev.");
public Task<Omika.Core.Contracts.Mobile.Sync.SyncOperationReceipt> SendOperationAsync(
Omika.Core.Contracts.Mobile.Sync.SyncOperation operation, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Sync.SyncStatusResponse> GetStatusAsync(
Guid operationId, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Snapshot.MobileSnapshotResponse> GetSnapshotAsync(CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Documents.DocumentDownloadResponse> DownloadDocumentAsync(
Guid documentGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Documents.DocumentUploadResponse> UploadDocumentAsync(
Omika.Core.Contracts.Mobile.Documents.DocumentUploadRequest request, CancellationToken ct = default)
=> throw new NotImplementedException();
}
/// <summary>
/// Client API qui enregistre les appels pour vérifier le comportement normal.
/// </summary>
private sealed class RecordingApiClient : IMobileApiClient
{
public string? AccessToken { get; set; }
public int ActivateCalls { get; private set; }
public LicenseActivateRequest? LastActivateRequest { get; private set; }
public Task<Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse> ActivateAsync(
LicenseActivateRequest request, CancellationToken ct = default)
{
ActivateCalls++;
LastActivateRequest = request;
return Task.FromResult(new Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse
{
License = new LicenseResponse { IsValid = false, ErrorCode = "E1001", ErrorMessage = "Licence inconnue" }
});
}
public Task<Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse> ValidateAsync(
LicenseValidateRequest request, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.License.MobileLicenseSessionResponse> GetDevelopmentSessionAsync(string deviceId, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Bootstrap.MobileBootstrapResponse> GetBootstrapAsync(CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Sync.SyncOperationReceipt> SendOperationAsync(
Omika.Core.Contracts.Mobile.Sync.SyncOperation operation, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Sync.SyncStatusResponse> GetStatusAsync(
Guid operationId, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Snapshot.MobileSnapshotResponse> GetSnapshotAsync(CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Documents.DocumentDownloadResponse> DownloadDocumentAsync(
Guid documentGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<Omika.Core.Contracts.Mobile.Documents.DocumentUploadResponse> UploadDocumentAsync(
Omika.Core.Contracts.Mobile.Documents.DocumentUploadRequest request, CancellationToken ct = default)
=> throw new NotImplementedException();
}
}