feat(sprint-7 P1-NEW-15 2026-04-25): PI-SPI Readiness check (8 vérifications) + endpoint admin
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 3m46s

Service ops/compliance pour valider l'état d'intégration PI-SPI BCEAO avant activation production.

PispiReadinessService — 8 vérifications structurées
- OAUTH2_CREDENTIALS (BLOCKING) : client_id + client_secret
- API_KEY (BLOCKING) : header X-API-Key obligatoire BCEAO
- MTLS_KEYSTORE (BLOCKING) : path + password + fichier .p12 existant
- MTLS_TRUSTSTORE (WARNING) : configuré + fichier existant (fallback cacerts si absent)
- WEBHOOK_SECRET (BLOCKING) : HMAC-SHA256 webhooks
- BASE_URL (WARNING) : sandbox vs production détecté automatiquement
- TAUX_EUR_XOF (WARNING) : taux récent ≤ 7 jours (sinon obsolète)
- PROVIDER_CONFIGURED (BLOCKING) : PispiAuth.isConfigured() = true (synthèse)

Statut global agrégé
- READY — tous PASS
- DEGRADED — uniquement WARNING en échec (sandbox OK, prod à risque)
- BLOCKED — au moins un BLOCKING en échec (sandbox impossible)

Endpoint /api/admin/pispi/readiness
- @RolesAllowed SUPER_ADMIN, COMPLIANCE_OFFICER
- HTTP 200 si READY/DEGRADED, 503 si BLOCKED
- Réponse JSON : globalStatus, baseUrl, checks détaillés, blockingIssues, warnings

Helpers publics ajoutés (pour readiness sans casser l'encapsulation)
- PispiAuth : hasClientId(), hasClientSecret(), hasApiKey(), keystorePath(), keystorePassword(), truststorePath(), truststorePassword()
- PispiSignatureVerifier : hasWebhookSecret()

Tests (15/15 verts)
- BLOCKED tout vide, READY tout configuré, DEGRADED warnings only
- Checks individuels OAuth2/ApiKey/Keystore (path absent / fichier inexistant / OK), Truststore warning, Webhook, BaseUrl sandbox/prod, Taux récent/obsolète, Provider
This commit is contained in:
dahoud
2026-04-25 10:48:59 +00:00
parent 86842f27af
commit 4d400dc48d
5 changed files with 564 additions and 0 deletions

View File

@@ -117,6 +117,23 @@ public class PispiAuth {
return apiKey;
}
// ── Readiness inspection helpers (P1-NEW-15) ────────────────────────────
public boolean hasClientId() { return clientId != null && !clientId.isEmpty(); }
public boolean hasClientSecret() { return clientSecret != null && !clientSecret.isEmpty(); }
public boolean hasApiKey() { return apiKey != null && !apiKey.isEmpty(); }
public java.util.Optional<String> keystorePath() {
return keystorePathOpt.filter(s -> !s.isBlank());
}
public java.util.Optional<String> keystorePassword() {
return keystorePasswordOpt.filter(s -> !s.isBlank());
}
public java.util.Optional<String> truststorePath() {
return truststorePathOpt.filter(s -> !s.isBlank());
}
public java.util.Optional<String> truststorePassword() {
return truststorePasswordOpt.filter(s -> !s.isBlank());
}
/** Base URL configurée (sandbox ou production). */
public String getBaseUrl() {
return baseUrl;

View File

@@ -30,6 +30,11 @@ public class PispiSignatureVerifier {
allowedIps = allowedIpsOpt.orElse("");
}
/** Readiness helper (P1-NEW-15) — TRUE si webhook secret HMAC est configuré. */
public boolean hasWebhookSecret() {
return webhookSecret != null && !webhookSecret.isEmpty();
}
public boolean isIpAllowed(String ip) {
if (allowedIps == null || allowedIps.isBlank()) {
return true;

View File

@@ -0,0 +1,42 @@
package dev.lions.unionflow.server.payment.pispi.readiness;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.ReadinessReport;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.ReadinessStatus;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
/**
* Endpoint d'inspection PI-SPI Readiness — usage ops/compliance avant activation production.
*
* <p>Status HTTP miroir du {@link ReadinessStatus} :
* <ul>
* <li>200 — READY (tout configuré, prêt pour prod)</li>
* <li>200 — DEGRADED (warnings non bloquants — sandbox OK, prod à risque)</li>
* <li>503 — BLOCKED (au moins un blocage critique — sandbox impossible)</li>
* </ul>
*
* @since 2026-04-25 (P1-NEW-15)
*/
@Path("/api/admin/pispi/readiness")
@Produces(MediaType.APPLICATION_JSON)
@Authenticated
public class PispiReadinessResource {
@Inject PispiReadinessService readinessService;
@GET
@RolesAllowed({"SUPER_ADMIN", "COMPLIANCE_OFFICER"})
public Response getReadiness() {
ReadinessReport report = readinessService.verifierReadiness();
int status = report.globalStatus() == ReadinessStatus.BLOCKED
? Response.Status.SERVICE_UNAVAILABLE.getStatusCode()
: Response.Status.OK.getStatusCode();
return Response.status(status).entity(report).build();
}
}

View File

@@ -0,0 +1,226 @@
package dev.lions.unionflow.server.payment.pispi.readiness;
import dev.lions.unionflow.server.entity.Devise;
import dev.lions.unionflow.server.payment.pispi.PispiAuth;
import dev.lions.unionflow.server.payment.pispi.PispiSignatureVerifier;
import dev.lions.unionflow.server.repository.TauxChangeRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
/**
* Service de vérification des pré-requis PI-SPI BCEAO avant activation production.
*
* <p>Permet à l'équipe ops/compliance de valider en un coup d'œil que tous les facteurs
* sont en place avant l'activation de l'intégration PI-SPI sandbox/production.
*
* <p>8 vérifications réalisées :
* <ul>
* <li>OAuth2 client credentials (client_id + client_secret)</li>
* <li>X-API-Key API Business</li>
* <li>mTLS keystore présent (path + password)</li>
* <li>Truststore présent (optionnel mais recommandé)</li>
* <li>Webhook signature secret (HMAC-SHA256)</li>
* <li>Base URL configurée (sandbox vs production)</li>
* <li>Taux de change EUR/XOF récent (≤ 7 jours)</li>
* <li>Provider PI-SPI globalement configuré ({@link PispiAuth#isConfigured()})</li>
* </ul>
*
* @since 2026-04-25 (P1-NEW-15)
*/
@ApplicationScoped
public class PispiReadinessService {
private static final Logger LOG = Logger.getLogger(PispiReadinessService.class);
@Inject PispiAuth pispiAuth;
@Inject PispiSignatureVerifier signatureVerifier;
@Inject TauxChangeRepository tauxChangeRepository;
@ConfigProperty(name = "pispi.api.base-url",
defaultValue = "https://sandbox.pispi.bceao.int/business-api/v1")
String baseUrl;
/**
* Exécute tous les checks et retourne un rapport structuré.
*
* @return rapport synthèse + détail par check
*/
public ReadinessReport verifierReadiness() {
List<CheckResult> checks = new ArrayList<>();
checks.add(verifierOAuth2());
checks.add(verifierApiKey());
checks.add(verifierMtlsKeystore());
checks.add(verifierTruststore());
checks.add(verifierWebhookSecret());
checks.add(verifierBaseUrl());
checks.add(verifierTauxEurXof());
checks.add(verifierProviderConfigured());
ReadinessStatus globalStatus = computeGlobalStatus(checks);
List<String> blocking = checks.stream()
.filter(c -> c.severity() == Severity.BLOCKING && c.status() == CheckStatus.FAIL)
.map(c -> c.name() + "" + c.message())
.toList();
List<String> warnings = checks.stream()
.filter(c -> c.severity() == Severity.WARNING && c.status() == CheckStatus.FAIL)
.map(c -> c.name() + "" + c.message())
.toList();
LOG.infof("PI-SPI Readiness : %s — blocking=%d, warnings=%d",
globalStatus, blocking.size(), warnings.size());
return new ReadinessReport(globalStatus, baseUrl, checks, blocking, warnings);
}
// ────────────────────────────────────────────────────────────
// Checks
// ────────────────────────────────────────────────────────────
CheckResult verifierOAuth2() {
boolean ok = pispiAuth.hasClientId() && pispiAuth.hasClientSecret();
return ok
? CheckResult.pass("OAUTH2_CREDENTIALS", Severity.BLOCKING,
"client_id et client_secret configurés")
: CheckResult.fail("OAUTH2_CREDENTIALS", Severity.BLOCKING,
"Manquant : pispi.api.client-id et/ou pispi.api.client-secret");
}
CheckResult verifierApiKey() {
return pispiAuth.hasApiKey()
? CheckResult.pass("API_KEY", Severity.BLOCKING,
"X-API-Key configurée")
: CheckResult.fail("API_KEY", Severity.BLOCKING,
"Manquant : pispi.api.api-key (header X-API-Key obligatoire BCEAO)");
}
CheckResult verifierMtlsKeystore() {
var pathOpt = pispiAuth.keystorePath();
var pwdOpt = pispiAuth.keystorePassword();
if (pathOpt.isEmpty() || pwdOpt.isEmpty()) {
return CheckResult.fail("MTLS_KEYSTORE", Severity.BLOCKING,
"Manquant : pispi.api.tls.keystore-path et/ou keystore-password (PKCS12 client cert)");
}
String path = pathOpt.get();
if (!Files.exists(Path.of(path))) {
return CheckResult.fail("MTLS_KEYSTORE", Severity.BLOCKING,
"Keystore introuvable au chemin : " + path);
}
return CheckResult.pass("MTLS_KEYSTORE", Severity.BLOCKING,
"Keystore PKCS12 présent : " + path);
}
CheckResult verifierTruststore() {
var pathOpt = pispiAuth.truststorePath();
if (pathOpt.isEmpty()) {
return CheckResult.fail("MTLS_TRUSTSTORE", Severity.WARNING,
"Truststore non configuré — fallback sur cacerts JVM (acceptable mais non recommandé)");
}
String path = pathOpt.get();
if (!Files.exists(Path.of(path))) {
return CheckResult.fail("MTLS_TRUSTSTORE", Severity.WARNING,
"Truststore introuvable au chemin : " + path);
}
return CheckResult.pass("MTLS_TRUSTSTORE", Severity.WARNING,
"Truststore présent : " + path);
}
CheckResult verifierWebhookSecret() {
return signatureVerifier.hasWebhookSecret()
? CheckResult.pass("WEBHOOK_SECRET", Severity.BLOCKING,
"Webhook HMAC secret configuré")
: CheckResult.fail("WEBHOOK_SECRET", Severity.BLOCKING,
"Manquant : pispi.webhook.secret (signature HMAC-SHA256 webhooks)");
}
CheckResult verifierBaseUrl() {
if (baseUrl == null || baseUrl.isBlank()) {
return CheckResult.fail("BASE_URL", Severity.BLOCKING,
"Base URL PI-SPI non configurée");
}
boolean isSandbox = baseUrl.contains("sandbox") || baseUrl.contains("dev");
String env = isSandbox ? "SANDBOX" : "PRODUCTION";
return CheckResult.pass("BASE_URL", Severity.WARNING,
"Base URL : " + baseUrl + " (" + env + ")");
}
CheckResult verifierTauxEurXof() {
LocalDate dateLimite = LocalDate.now().minusDays(7);
var taux = tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now());
if (taux.isEmpty()) {
return CheckResult.fail("TAUX_EUR_XOF", Severity.WARNING,
"Aucun taux EUR/XOF disponible — conversion impossible (parité fixe BCEAO devrait être seedée)");
}
if (taux.get().getDateValidite().isBefore(dateLimite)) {
return CheckResult.fail("TAUX_EUR_XOF", Severity.WARNING,
"Taux EUR/XOF obsolète (" + taux.get().getDateValidite() + ") — synchroniser");
}
return CheckResult.pass("TAUX_EUR_XOF", Severity.WARNING,
"Taux EUR/XOF récent : " + taux.get().getTaux() + " (date " + taux.get().getDateValidite() + ")");
}
CheckResult verifierProviderConfigured() {
return pispiAuth.isConfigured()
? CheckResult.pass("PROVIDER_CONFIGURED", Severity.BLOCKING,
"PispiAuth.isConfigured() = true — provider en mode RÉEL")
: CheckResult.fail("PROVIDER_CONFIGURED", Severity.BLOCKING,
"PispiAuth.isConfigured() = false — provider en mode MOCK (un facteur manque)");
}
// ────────────────────────────────────────────────────────────
// Status global
// ────────────────────────────────────────────────────────────
private ReadinessStatus computeGlobalStatus(List<CheckResult> checks) {
boolean anyBlockingFail = checks.stream()
.anyMatch(c -> c.severity() == Severity.BLOCKING && c.status() == CheckStatus.FAIL);
if (anyBlockingFail) return ReadinessStatus.BLOCKED;
boolean anyWarning = checks.stream()
.anyMatch(c -> c.severity() == Severity.WARNING && c.status() == CheckStatus.FAIL);
if (anyWarning) return ReadinessStatus.DEGRADED;
return ReadinessStatus.READY;
}
// ────────────────────────────────────────────────────────────
// DTOs
// ────────────────────────────────────────────────────────────
public enum ReadinessStatus { READY, DEGRADED, BLOCKED }
public enum CheckStatus { PASS, FAIL }
public enum Severity { BLOCKING, WARNING }
public record ReadinessReport(
ReadinessStatus globalStatus,
String baseUrl,
List<CheckResult> checks,
List<String> blockingIssues,
List<String> warnings
) {}
public record CheckResult(
String name,
CheckStatus status,
Severity severity,
String message
) {
public static CheckResult pass(String name, Severity severity, String message) {
return new CheckResult(name, CheckStatus.PASS, severity, message);
}
public static CheckResult fail(String name, Severity severity, String message) {
return new CheckResult(name, CheckStatus.FAIL, severity, message);
}
}
}

View File

@@ -0,0 +1,274 @@
package dev.lions.unionflow.server.payment.pispi.readiness;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import dev.lions.unionflow.server.entity.Devise;
import dev.lions.unionflow.server.entity.TauxChange;
import dev.lions.unionflow.server.payment.pispi.PispiAuth;
import dev.lions.unionflow.server.payment.pispi.PispiSignatureVerifier;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.CheckStatus;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.ReadinessStatus;
import dev.lions.unionflow.server.payment.pispi.readiness.PispiReadinessService.Severity;
import dev.lions.unionflow.server.repository.TauxChangeRepository;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class PispiReadinessServiceTest {
@Mock TauxChangeRepository tauxChangeRepository;
private PispiReadinessService service;
private PispiAuth auth;
private PispiSignatureVerifier verifier;
@BeforeEach
void setUp() throws Exception {
auth = new PispiAuth();
setField(auth, "clientId", "");
setField(auth, "clientSecret", "");
setField(auth, "apiKey", "");
setField(auth, "keystorePathOpt", Optional.<String>empty());
setField(auth, "keystorePasswordOpt", Optional.<String>empty());
setField(auth, "truststorePathOpt", Optional.<String>empty());
setField(auth, "truststorePasswordOpt", Optional.<String>empty());
verifier = new PispiSignatureVerifier();
setField(verifier, "webhookSecret", "");
service = new PispiReadinessService();
setField(service, "pispiAuth", auth);
setField(service, "signatureVerifier", verifier);
setField(service, "tauxChangeRepository", tauxChangeRepository);
setField(service, "baseUrl", "https://sandbox.pispi.bceao.int/business-api/v1");
}
private void setField(Object target, String fieldName, Object value) throws Exception {
Field f = findField(target.getClass(), fieldName);
f.setAccessible(true);
f.set(target, value);
}
private Field findField(Class<?> cls, String name) throws NoSuchFieldException {
Class<?> c = cls;
while (c != null) {
try {
return c.getDeclaredField(name);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
throw new NoSuchFieldException(name);
}
private TauxChange tauxRecent() {
TauxChange t = new TauxChange();
t.setDeviseSource(Devise.EUR);
t.setDeviseCible(Devise.XOF);
t.setTaux(new BigDecimal("655.957"));
t.setDateValidite(LocalDate.now());
return t;
}
// ── Tout vide ────────────────────────────────────────────────────────────
@Test
@DisplayName("Tout manquant → BLOCKED + checks failed")
void toutManquant_blocked() {
lenient().when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
.thenReturn(Optional.empty());
var report = service.verifierReadiness();
assertThat(report.globalStatus()).isEqualTo(ReadinessStatus.BLOCKED);
assertThat(report.checks()).hasSize(8);
assertThat(report.blockingIssues()).hasSizeGreaterThanOrEqualTo(4);
}
// ── Tout configuré ──────────────────────────────────────────────────────
@Test
@DisplayName("Tout configuré + keystore valide + taux récent → READY")
void toutOk_ready(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tmp) throws Exception {
java.nio.file.Path keystore = tmp.resolve("client.p12");
java.nio.file.Files.writeString(keystore, "fake-keystore");
java.nio.file.Path truststore = tmp.resolve("trust.p12");
java.nio.file.Files.writeString(truststore, "fake-trust");
setField(auth, "clientId", "client-uuid");
setField(auth, "clientSecret", "secret");
setField(auth, "apiKey", "api-key-xyz");
setField(auth, "keystorePathOpt", Optional.of(keystore.toString()));
setField(auth, "keystorePasswordOpt", Optional.of("kspw"));
setField(auth, "truststorePathOpt", Optional.of(truststore.toString()));
setField(auth, "truststorePasswordOpt", Optional.of("tspw"));
setField(verifier, "webhookSecret", "webhook-hmac-secret");
when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
.thenReturn(Optional.of(tauxRecent()));
var report = service.verifierReadiness();
assertThat(report.globalStatus()).isEqualTo(ReadinessStatus.READY);
assertThat(report.blockingIssues()).isEmpty();
assertThat(report.warnings()).isEmpty();
assertThat(report.checks()).allMatch(c -> c.status() == CheckStatus.PASS);
}
// ── DEGRADED ─────────────────────────────────────────────────────────────
@Test
@DisplayName("Tout BLOCKING ok mais taux EUR/XOF absent → DEGRADED")
void blockingOkWarningKO_degraded(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tmp)
throws Exception {
java.nio.file.Path keystore = tmp.resolve("client.p12");
java.nio.file.Files.writeString(keystore, "fake");
java.nio.file.Path truststore = tmp.resolve("trust.p12");
java.nio.file.Files.writeString(truststore, "fake");
setField(auth, "clientId", "c");
setField(auth, "clientSecret", "s");
setField(auth, "apiKey", "k");
setField(auth, "keystorePathOpt", Optional.of(keystore.toString()));
setField(auth, "keystorePasswordOpt", Optional.of("p"));
setField(auth, "truststorePathOpt", Optional.of(truststore.toString()));
setField(auth, "truststorePasswordOpt", Optional.of("p"));
setField(verifier, "webhookSecret", "secret");
when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
.thenReturn(Optional.empty()); // ← warning
var report = service.verifierReadiness();
assertThat(report.globalStatus()).isEqualTo(ReadinessStatus.DEGRADED);
assertThat(report.blockingIssues()).isEmpty();
assertThat(report.warnings()).anyMatch(w -> w.contains("TAUX_EUR_XOF"));
}
// ── Checks individuels ───────────────────────────────────────────────────
@Test
@DisplayName("verifierOAuth2 — fail si client_id vide")
void oauth2_failClientIdVide() throws Exception {
setField(auth, "clientId", "");
setField(auth, "clientSecret", "secret");
var r = service.verifierOAuth2();
assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
assertThat(r.severity()).isEqualTo(Severity.BLOCKING);
}
@Test
@DisplayName("verifierOAuth2 — pass si client_id et client_secret")
void oauth2_passComplet() throws Exception {
setField(auth, "clientId", "abc");
setField(auth, "clientSecret", "def");
var r = service.verifierOAuth2();
assertThat(r.status()).isEqualTo(CheckStatus.PASS);
}
@Test
@DisplayName("verifierApiKey — fail si vide, pass sinon")
void apiKey() throws Exception {
setField(auth, "apiKey", "");
assertThat(service.verifierApiKey().status()).isEqualTo(CheckStatus.FAIL);
setField(auth, "apiKey", "key");
assertThat(service.verifierApiKey().status()).isEqualTo(CheckStatus.PASS);
}
@Test
@DisplayName("verifierMtlsKeystore — fail si chemin absent")
void keystore_failPathAbsent() throws Exception {
setField(auth, "keystorePathOpt", Optional.<String>empty());
setField(auth, "keystorePasswordOpt", Optional.of("p"));
assertThat(service.verifierMtlsKeystore().status()).isEqualTo(CheckStatus.FAIL);
}
@Test
@DisplayName("verifierMtlsKeystore — fail si fichier inexistant")
void keystore_failFichierInexistant() throws Exception {
setField(auth, "keystorePathOpt", Optional.of("/nonexistent/keystore.p12"));
setField(auth, "keystorePasswordOpt", Optional.of("p"));
var r = service.verifierMtlsKeystore();
assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
assertThat(r.message()).contains("introuvable");
}
@Test
@DisplayName("verifierMtlsKeystore — pass si path + password + fichier existe")
void keystore_passComplet(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tmp)
throws Exception {
java.nio.file.Path k = tmp.resolve("k.p12");
java.nio.file.Files.writeString(k, "f");
setField(auth, "keystorePathOpt", Optional.of(k.toString()));
setField(auth, "keystorePasswordOpt", Optional.of("pw"));
assertThat(service.verifierMtlsKeystore().status()).isEqualTo(CheckStatus.PASS);
}
@Test
@DisplayName("verifierTruststore — warning si non configuré")
void truststore_warningSiAbsent() throws Exception {
setField(auth, "truststorePathOpt", Optional.<String>empty());
var r = service.verifierTruststore();
assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
assertThat(r.severity()).isEqualTo(Severity.WARNING);
}
@Test
@DisplayName("verifierWebhookSecret — fail si vide")
void webhook_failVide() throws Exception {
setField(verifier, "webhookSecret", "");
assertThat(service.verifierWebhookSecret().status()).isEqualTo(CheckStatus.FAIL);
}
@Test
@DisplayName("verifierBaseUrl — distingue sandbox / production")
void baseUrl_environnement() throws Exception {
setField(service, "baseUrl", "https://sandbox.pispi.bceao.int/business-api/v1");
var rSandbox = service.verifierBaseUrl();
assertThat(rSandbox.status()).isEqualTo(CheckStatus.PASS);
assertThat(rSandbox.message()).contains("SANDBOX");
setField(service, "baseUrl", "https://api.pispi.bceao.int/business-api/v1");
var rProd = service.verifierBaseUrl();
assertThat(rProd.message()).contains("PRODUCTION");
}
@Test
@DisplayName("verifierTauxEurXof — fail si taux > 7 jours")
void tauxEurXof_failObsolete() {
TauxChange vieux = new TauxChange();
vieux.setDateValidite(LocalDate.now().minusDays(15));
vieux.setTaux(new BigDecimal("655.957"));
when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
.thenReturn(Optional.of(vieux));
var r = service.verifierTauxEurXof();
assertThat(r.status()).isEqualTo(CheckStatus.FAIL);
assertThat(r.message()).contains("obsolète");
}
@Test
@DisplayName("verifierTauxEurXof — pass si taux récent")
void tauxEurXof_passRecent() {
when(tauxChangeRepository.trouverPlusRecent(Devise.EUR, Devise.XOF, LocalDate.now()))
.thenReturn(Optional.of(tauxRecent()));
assertThat(service.verifierTauxEurXof().status()).isEqualTo(CheckStatus.PASS);
}
@Test
@DisplayName("verifierProviderConfigured — fail si PispiAuth.isConfigured = false")
void providerConfigured_fail() throws Exception {
setField(auth, "clientId", ""); // not configured
assertThat(service.verifierProviderConfigured().status()).isEqualTo(CheckStatus.FAIL);
}
}