Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Omika.App.Local.Abstractions;
|
||||
using Omika.App.Local.Models;
|
||||
using Omika.App.Local.Repositories;
|
||||
using Omika.App.Local.Services;
|
||||
using Omika.App.Local.Sqlite;
|
||||
using Omika.Core.Contracts.Mobile.License;
|
||||
using Omika.Core.Services.Mobile;
|
||||
|
||||
namespace Omika.Tests.Mobile.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Tests de bout en bout de l'inscription sans réseau réel : QR → licence → API →
|
||||
/// stockage → redémarrage → bootstrap, ainsi que le scénario multi-client et le
|
||||
/// reset de changement de client (aucune contamination d'URL).
|
||||
/// </summary>
|
||||
public class EnrollmentEndToEndTests : LocalTestBase
|
||||
{
|
||||
private const string Kid = "test-kid-1";
|
||||
|
||||
private sealed class FakeLicenseEnrollmentClient : ILicenseEnrollmentClient
|
||||
{
|
||||
public MobileLicenseSessionResponse ActivateResponse { get; set; } = new();
|
||||
|
||||
public Task<MobileLicenseSessionResponse> ActivateAsync(
|
||||
string licenseServiceUrl, LicenseActivateRequest request, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(ActivateResponse);
|
||||
|
||||
public Task<MobileLicenseSessionResponse> ValidateAsync(
|
||||
string licenseServiceUrl, LicenseValidateRequest request, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new MobileLicenseSessionResponse());
|
||||
}
|
||||
|
||||
private sealed class FakeDeviceIdProvider : IDeviceIdProvider
|
||||
{
|
||||
public Task<string> GetDeviceIdAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult("device-1");
|
||||
}
|
||||
|
||||
private sealed class BootstrapCapturingHandler : HttpMessageHandler
|
||||
{
|
||||
public string? LastUri { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastUri = request.RequestUri?.ToString();
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("{}", Encoding.UTF8, "application/json")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static (string qr, IReadOnlyDictionary<string, string> registry) CreateQr(string code, string licenceUrl)
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var registry = new Dictionary<string, string> { [Kid] = rsa.ToXmlString(false) };
|
||||
string qr = EnrollmentQrCodec.Sign(
|
||||
EnrollmentQrCodec.BuildPayloadJson(Kid, code, "LIC-001", licenceUrl),
|
||||
rsa.ToXmlString(true));
|
||||
return (qr, registry);
|
||||
}
|
||||
|
||||
private MobileEnrollmentService CreateService(
|
||||
MobileLicenseSessionResponse licenceResponse,
|
||||
MobileEnrollmentStore store,
|
||||
MobileApiClient apiClient,
|
||||
IReadOnlyDictionary<string, string> registry)
|
||||
=> new(
|
||||
new FakeLicenseEnrollmentClient { ActivateResponse = licenceResponse },
|
||||
store,
|
||||
new FakeDeviceIdProvider(),
|
||||
new StubSessionService(),
|
||||
apiClient,
|
||||
registry);
|
||||
|
||||
private static MobileLicenseSessionResponse ValidLicence(string apiUrl)
|
||||
=> new()
|
||||
{
|
||||
License = new LicenseResponse { IsValid = true, ApiUrl = apiUrl },
|
||||
AccessToken = "token-e2e"
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_UrlBootstrap_Egale_UrlRetourneeParLicence()
|
||||
{
|
||||
var store = new MobileEnrollmentStore(Database);
|
||||
var handler = new BootstrapCapturingHandler();
|
||||
var apiClient = new MobileApiClient(new HttpClient(handler));
|
||||
var (qr, registry) = CreateQr("BATIK.01", "https://licence-a.example.com");
|
||||
|
||||
var service = CreateService(ValidLicence("https://api-a.example.com"), store, apiClient, registry);
|
||||
EnrollmentResult result = await service.EnrollAsync(qr);
|
||||
Assert.True(result.Success);
|
||||
|
||||
// Redémarrage simulé : nouvelle instance de service, même base locale.
|
||||
var restartApiClient = new MobileApiClient(new HttpClient(handler));
|
||||
var restartService = CreateService(ValidLicence("https://api-a.example.com"), store, restartApiClient, registry);
|
||||
MobileEnrollmentConfiguration? loaded = await restartService.LoadAsync();
|
||||
Assert.NotNull(loaded);
|
||||
restartApiClient.BaseUrl = loaded!.OmikaApiUrl;
|
||||
restartApiClient.AccessToken = "token-e2e";
|
||||
|
||||
await restartApiClient.GetBootstrapAsync();
|
||||
|
||||
Assert.Equal("https://api-a.example.com/api/mobile/bootstrap", handler.LastUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultiClient_ClientA_ClientB_UrlsDistinctesSansContamination()
|
||||
{
|
||||
// Deux bases locales distinctes (deux appareils) : même code, même binaire.
|
||||
using var dbA = new LocalDatabase(new LocalDatabaseOptions());
|
||||
using var dbB = new LocalDatabase(new LocalDatabaseOptions());
|
||||
dbA.InitializeSchemaAsync().GetAwaiter().GetResult();
|
||||
dbB.InitializeSchemaAsync().GetAwaiter().GetResult();
|
||||
|
||||
var storeA = new MobileEnrollmentStore(dbA);
|
||||
var storeB = new MobileEnrollmentStore(dbB);
|
||||
var apiA = new MobileApiClient(new HttpClient());
|
||||
var apiB = new MobileApiClient(new HttpClient());
|
||||
|
||||
var (qrA, regA) = CreateQr("CLIENT.A", "https://licence-a.example.com");
|
||||
var (qrB, regB) = CreateQr("CLIENT.B", "https://licence-b.example.com");
|
||||
|
||||
EnrollmentResult resA = await CreateService(ValidLicence("https://api-a.example.com"), storeA, apiA, regA).EnrollAsync(qrA);
|
||||
EnrollmentResult resB = await CreateService(ValidLicence("https://api-b.example.com"), storeB, apiB, regB).EnrollAsync(qrB);
|
||||
|
||||
Assert.True(resA.Success);
|
||||
Assert.True(resB.Success);
|
||||
Assert.Equal("https://api-a.example.com", apiA.BaseUrl);
|
||||
Assert.Equal("https://api-b.example.com", apiB.BaseUrl);
|
||||
Assert.Equal("https://api-a.example.com", (await storeA.LoadAsync())!.OmikaApiUrl);
|
||||
Assert.Equal("https://api-b.example.com", (await storeB.LoadAsync())!.OmikaApiUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reset_ClientA_Vers_ClientB_AucuneReutilisationApiA()
|
||||
{
|
||||
var store = new MobileEnrollmentStore(Database);
|
||||
var apiClient = new MobileApiClient(new HttpClient());
|
||||
var (qrA, regA) = CreateQr("CLIENT.A", "https://licence-a.example.com");
|
||||
|
||||
// Inscription Client A.
|
||||
await CreateService(ValidLicence("https://api-a.example.com"), store, apiClient, regA).EnrollAsync(qrA);
|
||||
Assert.Equal("https://api-a.example.com", apiClient.BaseUrl);
|
||||
|
||||
// Reset complet (désinscription) : table + token + client API réinitialisés.
|
||||
var tokenStore = new FakeTokenStore();
|
||||
var maintenance = new LocalDataMaintenanceService(Database, "unused", tokenStore);
|
||||
await maintenance.ResetAsync();
|
||||
apiClient.BaseUrl = null;
|
||||
apiClient.AccessToken = null;
|
||||
|
||||
Assert.Null(await store.LoadAsync());
|
||||
Assert.Null(apiClient.BaseUrl);
|
||||
Assert.True(tokenStore.Cleared);
|
||||
|
||||
// Inscription Client B.
|
||||
var (qrB, regB) = CreateQr("CLIENT.B", "https://licence-b.example.com");
|
||||
EnrollmentResult resB = await CreateService(ValidLicence("https://api-b.example.com"), store, apiClient, regB).EnrollAsync(qrB);
|
||||
|
||||
Assert.True(resB.Success);
|
||||
Assert.Equal("https://api-b.example.com", apiClient.BaseUrl);
|
||||
Assert.Equal("https://api-b.example.com", (await store.LoadAsync())!.OmikaApiUrl);
|
||||
Assert.NotEqual("https://api-a.example.com", apiClient.BaseUrl);
|
||||
}
|
||||
|
||||
private sealed class FakeTokenStore : ISecureTokenStore
|
||||
{
|
||||
public bool Cleared { get; private set; }
|
||||
|
||||
public Task SaveAsync(string token, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
public Task<string?> GetAsync(CancellationToken cancellationToken = default) => Task.FromResult<string?>(null);
|
||||
|
||||
public Task ClearAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Cleared = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user