382 lines
14 KiB
C#
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}");
|
|
}
|
|
}
|
|
|
|
|
|
|