Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user