187 lines
7.9 KiB
C#
187 lines
7.9 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|