Files
2026-10-23 18:46:50 +02:00

382 lines
14 KiB
C#

using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using Omika.App.Local.Services;
using Omika.Core.Contracts.Documents;
using Omika.Core.Contracts.Mobile.Bootstrap;
using Omika.Core.Contracts.Mobile.License;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests du MobileApiClient : activation, validation, bootstrap authentifié et
/// gestion des réponses d'erreur. Vérifie que le Bearer token est bien envoyé
/// sur les requêtes authentifiées et jamais loggé.
/// </summary>
public class MobileApiClientTests
{
private sealed class FakeHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, HttpResponseMessage> Responder { get; set; } = _ =>
new HttpResponseMessage(HttpStatusCode.OK);
public HttpRequestMessage? LastRequest { get; private set; }
public string? LastRequestBody { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
if (request.Content != null)
{
LastRequestBody = request.Content.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();
}
return Task.FromResult(Responder(request));
}
}
private static MobileApiClient CreateClient(FakeHandler handler, string baseUrl = "http://test.local")
{
var http = new HttpClient(handler);
return new MobileApiClient(http) { BaseUrl = baseUrl };
}
private static HttpResponseMessage JsonResponse(HttpStatusCode status, object body)
{
string json = JsonSerializer.Serialize(body, new JsonSerializerOptions(JsonSerializerDefaults.Web));
return new HttpResponseMessage(status)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
[Fact]
public async Task Activate_EnvoieRequeteEtDeserialise()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.OK, new MobileLicenseSessionResponse
{
License = new LicenseResponse { IsValid = true, CodeClient = "CLIENT01" },
AccessToken = "token-abc",
AccessTokenExpiresAtUtc = DateTimeOffset.UtcNow.AddHours(24)
})
};
var client = CreateClient(handler);
// Act
var result = await client.ActivateAsync(new LicenseActivateRequest
{
CodeClient = "CLIENT01",
NumeroLicence = "LIC-001",
DeviceId = "device-1"
});
// Assert
Assert.True(result.License.IsValid);
Assert.Equal("token-abc", result.AccessToken);
Assert.Equal("/api/license/activate", handler.LastRequest!.RequestUri!.AbsolutePath);
Assert.Contains("\"codeClient\":\"CLIENT01\"", handler.LastRequestBody);
Assert.Contains("\"deviceId\":\"device-1\"", handler.LastRequestBody);
}
[Fact]
public async Task Activate_Reponse400_DeserialiseErreur()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.BadRequest, new MobileLicenseSessionResponse
{
License = new LicenseResponse
{
IsValid = false,
ErrorCode = "E1001",
ErrorMessage = "Licence inconnue"
}
})
};
var client = CreateClient(handler);
// Act
var result = await client.ActivateAsync(new LicenseActivateRequest
{
CodeClient = "CLIENT01",
NumeroLicence = "LIC-999"
});
// Assert
Assert.False(result.License.IsValid);
Assert.Equal("E1001", result.License.ErrorCode);
Assert.Equal("Licence inconnue", result.License.ErrorMessage);
}
[Fact]
public async Task Validate_EnvoieRequete()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.OK, new MobileLicenseSessionResponse
{
License = new LicenseResponse { IsValid = true },
AccessToken = "token-validate"
})
};
var client = CreateClient(handler);
// Act
var result = await client.ValidateAsync(new LicenseValidateRequest
{
CodeClient = "CLIENT01",
NumeroLicence = "LIC-001",
DeviceId = "device-1"
});
// Assert
Assert.True(result.License.IsValid);
Assert.Equal("/api/license/validate", handler.LastRequest!.RequestUri!.AbsolutePath);
Assert.Contains("\"codeClient\":\"CLIENT01\"", handler.LastRequestBody);
}
[Fact]
public async Task Bootstrap_AvecBearerToken_EnvoieAuthorization()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.OK, new MobileBootstrapResponse
{
Utilisateur = new Omika.Core.Contracts.Security.CurrentUserDto { NomAffiche = "Dupont" }
})
};
var client = CreateClient(handler);
client.AccessToken = "token-secret";
// Act
var result = await client.GetBootstrapAsync();
// Assert : le Bearer token est envoyé.
Assert.Equal("/api/mobile/bootstrap", handler.LastRequest!.RequestUri!.AbsolutePath);
Assert.Equal("Bearer token-secret", handler.LastRequest.Headers.Authorization!.ToString());
Assert.Equal("Dupont", result.Utilisateur.NomAffiche);
}
[Fact]
public async Task Bootstrap_SansToken_PasDeAuthorization()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.OK, new MobileBootstrapResponse())
};
var client = CreateClient(handler);
// Act
await client.GetBootstrapAsync();
// Assert : pas d'en-tête Authorization sans token.
Assert.Null(handler.LastRequest!.Headers.Authorization);
}
[Fact]
public async Task Bootstrap_ErreurServeur_LeveException()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => new HttpResponseMessage(HttpStatusCode.Unauthorized)
};
var client = CreateClient(handler);
client.AccessToken = "token-expire";
// Act / Assert
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetBootstrapAsync());
}
[Fact]
public async Task Snapshot_AvecBearerToken_EnvoieAuthorization()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.OK, new Omika.Core.Contracts.Mobile.Snapshot.MobileSnapshotResponse
{
GeneratedAtUtc = DateTimeOffset.UtcNow,
Clients =
[
new Omika.Core.Contracts.Clients.ClientDetailsDto { Id = Guid.NewGuid().ToString(), Nom = "Client 1" }
]
})
};
var client = CreateClient(handler);
client.AccessToken = "token-snapshot";
// Act
var result = await client.GetSnapshotAsync();
// Assert : le Bearer token est envoyé sur /api/sync/snapshot et la
// réponse est correctement désérialisée.
Assert.Equal("/api/sync/snapshot", handler.LastRequest!.RequestUri!.AbsolutePath);
Assert.Equal("Bearer token-snapshot", handler.LastRequest.Headers.Authorization!.ToString());
Assert.NotEqual(default, result.GeneratedAtUtc);
Assert.Single(result.Clients);
Assert.Equal("Client 1", result.Clients[0].Nom);
}
[Fact]
public async Task Snapshot_ErreurServeur_LeveException()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
};
var client = CreateClient(handler);
client.AccessToken = "token-expire";
// Act / Assert : une erreur serveur lève une exception (l'ancienne base
// locale reste utilisable, gérée par le SyncEngine).
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetSnapshotAsync());
}
[Fact]
public async Task Snapshot_DeserialisationEchoue_LeveException()
{
// Arrange : réponse HTTP 200 mais corps JSON syntaxiquement invalide.
var handler = new FakeHandler
{
Responder = _ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""{"generatedAtUtc": "not-a-date", """, Encoding.UTF8, "application/json")
}
};
var client = CreateClient(handler);
client.AccessToken = "token-snapshot";
// Act / Assert : la désérialisation échoue → JsonException. Le SyncEngine
// capture cette exception et conserve l'ancienne base locale.
await Assert.ThrowsAsync<System.Text.Json.JsonException>(() => client.GetSnapshotAsync());
}
[Fact]
public async Task DevelopmentSession_EnvoieRequete()
{
// Arrange
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.OK, new MobileLicenseSessionResponse
{
License = new LicenseResponse { IsValid = true, CodeClient = "LOCAL-DEV" },
AccessToken = "token-dev"
})
};
var client = CreateClient(handler);
// Act
var result = await client.GetDevelopmentSessionAsync("device-test-1");
// Assert : l'endpoint Development est appelé et la session est désérialisée.
Assert.Equal("/api/dev/session", handler.LastRequest!.RequestUri!.AbsolutePath);
Assert.True(result.License.IsValid);
Assert.Equal("token-dev", result.AccessToken);
}
// =========================================================================
// Envoi d'email : capture du message serveur (au lieu du message technique
// « Response status code does not indicate success: 500 »).
// =========================================================================
[Fact]
public async Task SendEmail_Erreur500_AvecBodyJson_LeveMessageServeur()
{
// Arrange : le serveur renvoie un 500 structuré (message utilisateur +
// détail technique). Le client doit exposer le message, pas le code 500.
var handler = new FakeHandler
{
Responder = _ => JsonResponse(HttpStatusCode.InternalServerError, new
{
message = "Le serveur de messagerie a refusé l'envoi.",
detail = "5.7.1 relay denied",
type = "EnvoiRefuse"
})
};
var client = CreateClient(handler);
// Act
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.SendEmailByInterventionAsync(
Guid.NewGuid(), null, new CreateEmailInterventionRequest { Objet = "Test" }));
// Assert
Assert.Equal("Le serveur de messagerie a refusé l'envoi.", ex.Message);
}
[Fact]
public async Task SendEmail_Erreur400_TexteBrut_LeveTexteServeur()
{
// Arrange : le serveur renvoie un 400 texte brut (ex. « Sélectionnez une
// configuration SMTP... »).
var handler = new FakeHandler
{
Responder = _ => new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("Sélectionnez une configuration SMTP pour envoyer le courriel.", Encoding.UTF8, "text/plain")
}
};
var client = CreateClient(handler);
// Act
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.SendEmailByInterventionAsync(
Guid.NewGuid(), null, new CreateEmailInterventionRequest { Objet = "Test" }));
// Assert
Assert.Equal("Sélectionnez une configuration SMTP pour envoyer le courriel.", ex.Message);
}
[Fact]
public async Task SendEmail_Erreur500_SansBody_LeveMessageGenerique()
{
// Arrange : 500 sans corps (serveur sans gestionnaire d'erreur).
var handler = new FakeHandler
{
Responder = _ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
};
var client = CreateClient(handler);
// Act
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.SendEmailByInterventionAsync(
Guid.NewGuid(), null, new CreateEmailInterventionRequest { Objet = "Test" }));
// Assert
Assert.Equal("Impossible d'envoyer l'email.", ex.Message);
}
[Fact]
public void CreateHttpClient_BorneLeDelaiDesRequetes()
{
// Le défaut de HttpClient (100 s) laisserait l'écran en chargement plus d'une
// minute sur un réseau mobile dégradé, avant un échec de toute façon inévitable.
using HttpClient client = MobileApiClient.CreateHttpClient(new FakeHandler());
Assert.Equal(MobileApiClient.DefaultTimeout, client.Timeout);
Assert.True(
client.Timeout <= TimeSpan.FromSeconds(30),
$"Délai trop long pour un client mobile : {client.Timeout}");
Assert.True(
client.Timeout >= TimeSpan.FromSeconds(5),
$"Délai trop court pour un réseau mobile : {client.Timeout}");
}
}