347 lines
14 KiB
C#
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();
|
|
}
|
|
}
|
|
|
|
|