using System.Net; using System.Net.Http.Json; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Omika.Core.Abstractions; using Omika.Core.Compatibility; using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Interfaces.Mobile; using Omika.Core.Runtime; using Omika.Data.Configuration; using Omika.Data.Connection; using Omika.Data.Runtime; namespace Omika.Tests.Api; /// /// Tests HTTP des endpoints de synchronisation mobile. /// Vérifie l'authentification (sans token) et la réception d'une opération. /// [Collection("ApplicationRuntimeContext")] public class SyncEndpointsTests : IDisposable { private const string SigningKey = "test-signing-key-for-integration-tests-only"; private readonly FakeSyncOperationService _fakeService = new(); private readonly WebApplicationFactory _factory; public SyncEndpointsTests() { ApplicationRuntimeContext.Reset(); _factory = new WebApplicationFactory() .WithWebHostBuilder(builder => { builder.ConfigureServices(services => { var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Hfsql:Server"] = "test-server", ["Hfsql:DatabaseName"] = "test-db", ["MobileAuth:SigningKey"] = SigningKey }) .Build(); services.AddSingleton(config); services.RemoveAll(); services.AddScoped(); services.RemoveAll(); services.AddScoped(); services.RemoveAll(); services.AddScoped(_ => _fakeService); }); }); } public void Dispose() { _factory.Dispose(); ApplicationRuntimeContext.Reset(); } [Fact] public async Task Receive_WithoutToken_Returns401() { var client = _factory.CreateClient(); var response = await client.PostAsJsonAsync("/api/sync/operations", ValidOperation()); Assert.Equal(401, (int)response.StatusCode); } [Fact] public async Task Receive_ValidOperation_Returns200Received() { _fakeService.Receipt = new SyncOperationReceipt { OperationId = Guid.NewGuid(), Status = "RECEIVED", ReceivedAtUtc = DateTimeOffset.UtcNow }; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.PostAsJsonAsync("/api/sync/operations", ValidOperation()); Assert.Equal(200, (int)response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal("RECEIVED", body.Status); } [Fact] public async Task Receive_ValidOperation_PassesIdentityFromToken() { _fakeService.Receipt = new SyncOperationReceipt { OperationId = Guid.NewGuid(), Status = "RECEIVED", ReceivedAtUtc = DateTimeOffset.UtcNow }; var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", CreateValidToken()); var response = await client.PostAsJsonAsync("/api/sync/operations", ValidOperation()); Assert.Equal(200, (int)response.StatusCode); Assert.NotNull(_fakeService.LastIdentity); Assert.Equal("BATIK.01", _fakeService.LastIdentity.CodeClient); Assert.Equal("device-abc-123", _fakeService.LastIdentity.DeviceId); Assert.Equal("user-1", _fakeService.LastIdentity.UserId); } private static SyncOperation ValidOperation() { return new SyncOperation { OperationId = Guid.NewGuid(), DeviceId = "device-abc-123", EntityType = "Client", OperationType = SyncOperationType.Create, EntityGuid = Guid.NewGuid(), Payload = "{}" }; } private string CreateValidToken() { using var scope = _factory.Services.CreateScope(); var tokenService = scope.ServiceProvider.GetRequiredService(); return tokenService.GenerateToken(new MobileTokenIdentity { CodeClient = "BATIK.01", DeviceId = "device-abc-123", UserId = "user-1", CollaborateurId = "7", NumeroLicence = "1ab053eb5e274e1f92455bbb7de9f2d3" }); } // ===== Fakes ===== private sealed class FakeSyncOperationService : ISyncOperationService { public SyncOperationReceipt Receipt { get; set; } = new(); public MobileTokenIdentity? LastIdentity { get; private set; } public Task ReceiveAsync( SyncOperation operation, MobileTokenIdentity identity, CancellationToken cancellationToken = default) { LastIdentity = identity; return Task.FromResult(Receipt); } public Task GetStatusAsync( Guid operationId, string authenticatedDeviceId, CancellationToken cancellationToken = default) => Task.FromResult(new SyncStatusResponse { OperationId = operationId, Status = SyncStatus.StatusNotAvailable, DeviceId = authenticatedDeviceId }); } private sealed class FakeRuntimeInitializer : IDatabaseRuntimeInitializer { public Task InitializeAsync(CancellationToken cancellationToken = default) { ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); return Task.FromResult(Result.Success()); } } private sealed class FakeHfsqlConnection : IHfsqlConnection, IDisposable { public HfsqlOptions Options => new(); public HfsqlConnectionState State => HfsqlConnectionState.Closed; public void Dispose() { } public Task OpenAsync(CancellationToken ct = default) => Task.CompletedTask; public Task CloseAsync() => Task.CompletedTask; public Task ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => Task.FromResult(null); public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => Task.FromResult(0); public Task>> QueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken ct = default) => Task.FromResult>>([]); public Task> GetColumnsAsync(string tableName, CancellationToken ct = default) => Task.FromResult>([]); public ValueTask DisposeAsync() => ValueTask.CompletedTask; } }