diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 8675fe2..ba49713 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -12,71 +12,10 @@ env: GHCR_NAMESPACE: igmlcreation jobs: - # GATE : aucune image n'est build/push tant que les 3 suites unitaires - # (Java / Python / Angular) ne passent pas. Un tag posé sur un commit aux - # tests rouges ne publiera donc PAS d'images. - tests: - runs-on: ubuntu-latest - # Base PostgreSQL réelle pour les tests Core (joignable par le nom `postgres` - # depuis le conteneur du job Gitea). - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_DB: loremind_test - POSTGRES_USER: loremind_test - POSTGRES_PASSWORD: loremind_test - options: >- - --health-cmd "pg_isready -U loremind_test -d loremind_test" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - cache: maven - - name: Core — mvn test (+ JaCoCo check) - working-directory: core - env: - SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/loremind_test - SPRING_DATASOURCE_USERNAME: loremind_test - SPRING_DATASOURCE_PASSWORD: loremind_test - run: | - chmod +x ./mvnw - ./mvnw -B test - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: pip - cache-dependency-path: brain/requirements-dev.txt - - name: Brain — pytest (+ couverture) - working-directory: brain - run: | - pip install -r requirements-dev.txt - pytest --cov=app --cov-report=term-missing --cov-fail-under=50 - - - name: Set up Node 20 - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: npm - cache-dependency-path: web/package-lock.json - - name: Web — vitest (+ couverture) - working-directory: web - run: | - npm ci --no-audit --no-fund - npm run test:unit:coverage - + # NB : pas de job de test ici. Le gate qualité vit dans ci.yml (push main + PR) ; + # avec la branch protection Gitea (checks requis sur main), on ne tague donc que + # du code déjà vert. release.yml ne fait que builder/pousser les images. build: - needs: tests runs-on: ubuntu-latest strategy: fail-fast: false @@ -157,7 +96,6 @@ jobs: # donc uniquement sur les releases stables — pas la peine de re-publier # une variante beta du switcher, c'est une infrastructure neutre. build-switcher: - needs: tests runs-on: ubuntu-latest steps: - name: Checkout diff --git a/brain/app/main.py b/brain/app/main.py index 6c72c15..529987a 100644 --- a/brain/app/main.py +++ b/brain/app/main.py @@ -26,7 +26,7 @@ from app.infrastructure.ollama_model_installer import ensure_ollama_embedding_mo app = FastAPI( title="LoreMind Brain", description="Backend IA pour la génération de contenu narratif.", - version="0.16.2", + version="0.16.3", ) logger = logging.getLogger(__name__) diff --git a/core/pom.xml b/core/pom.xml index 753c790..5b9a17c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -14,7 +14,7 @@ com.loremind loremind-core - 0.16.2 + 0.16.3 LoreMind Core Backend Core - Architecture Hexagonale @@ -152,6 +152,45 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + properties + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off + + org.springframework.boot spring-boot-maven-plugin diff --git a/core/src/main/java/com/loremind/infrastructure/ai/BrainRulesImportClient.java b/core/src/main/java/com/loremind/infrastructure/ai/BrainRulesImportClient.java index c0bdb41..ea84e2a 100644 --- a/core/src/main/java/com/loremind/infrastructure/ai/BrainRulesImportClient.java +++ b/core/src/main/java/com/loremind/infrastructure/ai/BrainRulesImportClient.java @@ -225,7 +225,7 @@ public class BrainRulesImportClient implements RulesPdfImporter { private Map toStringMap(JsonNode object) { Map out = new LinkedHashMap<>(); if (object != null && object.isObject()) { - object.fields().forEachRemaining(e -> out.put(e.getKey(), e.getValue().asText())); + object.properties().forEach(e -> out.put(e.getKey(), e.getValue().asText())); } return out; } diff --git a/core/src/main/java/com/loremind/infrastructure/ai/RestTemplateConfig.java b/core/src/main/java/com/loremind/infrastructure/ai/RestTemplateConfig.java index 534127b..8c8bc4e 100644 --- a/core/src/main/java/com/loremind/infrastructure/ai/RestTemplateConfig.java +++ b/core/src/main/java/com/loremind/infrastructure/ai/RestTemplateConfig.java @@ -37,8 +37,8 @@ public class RestTemplateConfig { @Value("${brain.timeout-seconds}") long timeoutSeconds, @Value("${brain.internal-secret}") String internalSecret) { return builder - .setConnectTimeout(Duration.ofSeconds(10)) - .setReadTimeout(Duration.ofSeconds(timeoutSeconds)) + .connectTimeout(Duration.ofSeconds(10)) + .readTimeout(Duration.ofSeconds(timeoutSeconds)) .additionalInterceptors((request, body, execution) -> { if (internalSecret != null && !internalSecret.isBlank()) { request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret); @@ -61,8 +61,8 @@ public class RestTemplateConfig { @Value("${brain.import-timeout-seconds:600}") long importTimeoutSeconds, @Value("${brain.internal-secret}") String internalSecret) { return builder - .setConnectTimeout(Duration.ofSeconds(10)) - .setReadTimeout(Duration.ofSeconds(importTimeoutSeconds)) + .connectTimeout(Duration.ofSeconds(10)) + .readTimeout(Duration.ofSeconds(importTimeoutSeconds)) .additionalInterceptors((request, body, execution) -> { if (internalSecret != null && !internalSecret.isBlank()) { request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret); diff --git a/core/src/main/java/com/loremind/infrastructure/desktop/DesktopUpdateService.java b/core/src/main/java/com/loremind/infrastructure/desktop/DesktopUpdateService.java index 78e38a1..a7f02aa 100644 --- a/core/src/main/java/com/loremind/infrastructure/desktop/DesktopUpdateService.java +++ b/core/src/main/java/com/loremind/infrastructure/desktop/DesktopUpdateService.java @@ -50,8 +50,8 @@ public class DesktopUpdateService { @Value("${desktop.update.releases-api-url:https://api.github.com/repos/IGMLcreation/LoreMind/releases/latest}") String releasesApiUrl, @Nullable BuildProperties buildProperties) { this.http = builder - .setConnectTimeout(Duration.ofSeconds(5)) - .setReadTimeout(Duration.ofSeconds(10)) + .connectTimeout(Duration.ofSeconds(5)) + .readTimeout(Duration.ofSeconds(10)) .build(); this.enabled = enabled; this.releasesApiUrl = releasesApiUrl; diff --git a/core/src/main/java/com/loremind/infrastructure/licensing/HttpLicenseRelay.java b/core/src/main/java/com/loremind/infrastructure/licensing/HttpLicenseRelay.java index fa98958..f052cca 100644 --- a/core/src/main/java/com/loremind/infrastructure/licensing/HttpLicenseRelay.java +++ b/core/src/main/java/com/loremind/infrastructure/licensing/HttpLicenseRelay.java @@ -40,8 +40,8 @@ public class HttpLicenseRelay implements LicenseRelay { RestTemplateBuilder builder, @Value("${licensing.relay.base-url:}") String baseUrl) { this.http = builder - .setConnectTimeout(Duration.ofSeconds(5)) - .setReadTimeout(Duration.ofSeconds(15)) + .connectTimeout(Duration.ofSeconds(5)) + .readTimeout(Duration.ofSeconds(15)) .build(); this.baseUrl = stripTrailingSlash(baseUrl); } diff --git a/core/src/main/java/com/loremind/infrastructure/licensing/LicenseRefreshDaemon.java b/core/src/main/java/com/loremind/infrastructure/licensing/LicenseRefreshDaemon.java index d21e719..85aa828 100644 --- a/core/src/main/java/com/loremind/infrastructure/licensing/LicenseRefreshDaemon.java +++ b/core/src/main/java/com/loremind/infrastructure/licensing/LicenseRefreshDaemon.java @@ -7,6 +7,7 @@ import com.loremind.domain.licensing.RegistryCredentials; import com.loremind.domain.licensing.ports.DockerConfigWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -25,6 +26,8 @@ import java.util.Optional; * la plupart du temps. */ @Component +// Desactivable (ex: tests) : sans cette propriete (prod), le daemon tourne. +@ConditionalOnProperty(name = "licensing.refresh.enabled", matchIfMissing = true) public class LicenseRefreshDaemon { private static final Logger log = LoggerFactory.getLogger(LicenseRefreshDaemon.class); diff --git a/core/src/main/java/com/loremind/infrastructure/storage/MinioConfig.java b/core/src/main/java/com/loremind/infrastructure/storage/MinioConfig.java index dac2433..b11e47d 100644 --- a/core/src/main/java/com/loremind/infrastructure/storage/MinioConfig.java +++ b/core/src/main/java/com/loremind/infrastructure/storage/MinioConfig.java @@ -4,6 +4,8 @@ import io.minio.BucketExistsArgs; import io.minio.MakeBucketArgs; import io.minio.MinioClient; import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -24,6 +26,8 @@ import org.springframework.context.annotation.Configuration; @ConditionalOnProperty(name = "storage.backend", havingValue = "minio", matchIfMissing = true) public class MinioConfig { + private static final Logger log = LoggerFactory.getLogger(MinioConfig.class); + @Value("${minio.endpoint}") private String endpoint; @@ -66,12 +70,11 @@ public class MinioConfig { boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); - System.out.println("[MinIO] Bucket '" + bucket + "' cree."); + log.info("[MinIO] Bucket '{}' cree.", bucket); } } catch (Exception e) { - System.err.println("[MinIO] Initialisation impossible (endpoint=" + endpoint - + "). Les uploads d'images echoueront tant que MinIO n'est pas joignable. " - + "Cause : " + e.getMessage()); + log.warn("[MinIO] Initialisation impossible (endpoint={}). Les uploads d'images " + + "echoueront tant que MinIO n'est pas joignable. Cause : {}", endpoint, e.getMessage()); } } } diff --git a/core/src/main/java/com/loremind/infrastructure/updates/UpdateCheckService.java b/core/src/main/java/com/loremind/infrastructure/updates/UpdateCheckService.java index bb81920..b89aa30 100644 --- a/core/src/main/java/com/loremind/infrastructure/updates/UpdateCheckService.java +++ b/core/src/main/java/com/loremind/infrastructure/updates/UpdateCheckService.java @@ -74,8 +74,8 @@ public class UpdateCheckService { LicenseService licenseService, @Nullable BuildProperties buildProperties) { this.http = builder - .setConnectTimeout(Duration.ofSeconds(5)) - .setReadTimeout(Duration.ofSeconds(15)) + .connectTimeout(Duration.ofSeconds(5)) + .readTimeout(Duration.ofSeconds(15)) .build(); this.registry = normalizeRegistry(registry); this.images = parseImages(imagesCsv); diff --git a/core/src/main/resources/application.properties b/core/src/main/resources/application.properties index 0215f36..d0e25c0 100644 --- a/core/src/main/resources/application.properties +++ b/core/src/main/resources/application.properties @@ -29,6 +29,9 @@ spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true +# Fixe explicitement open-in-view (defaut Spring = true) : meme comportement, +# mais supprime l'avertissement "spring.jpa.open-in-view is enabled by default". +spring.jpa.open-in-view=true # ============================================================================ # Flyway : migrations de schema versionnees (src/main/resources/db/migration). diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/AiChatControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/AiChatControllerTest.java index 6be7d72..3ff97d1 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/AiChatControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/AiChatControllerTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.core.task.TaskExecutor; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -53,10 +53,10 @@ class AiChatControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; - @MockBean private StreamChatForLoreUseCase loreUseCase; - @MockBean private StreamChatForCampaignUseCase campaignUseCase; - @MockBean private StreamChatForSessionUseCase sessionUseCase; - @MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; + @MockitoBean private StreamChatForLoreUseCase loreUseCase; + @MockitoBean private StreamChatForCampaignUseCase campaignUseCase; + @MockitoBean private StreamChatForSessionUseCase sessionUseCase; + @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @BeforeEach void setUp() { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignAdaptControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignAdaptControllerTest.java index 8295038..6242da1 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignAdaptControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignAdaptControllerTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.core.task.TaskExecutor; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; @@ -42,8 +42,8 @@ class CampaignAdaptControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private CampaignAdaptService campaignAdaptService; - @MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; + @MockitoBean private CampaignAdaptService campaignAdaptService; + @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @BeforeEach void setUp() { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignImportControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignImportControllerTest.java index b4bb1a7..dbc3acf 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignImportControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/CampaignImportControllerTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.core.task.TaskExecutor; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; @@ -58,8 +58,8 @@ class CampaignImportControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; - @MockBean private CampaignImportService campaignImportService; - @MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; + @MockitoBean private CampaignImportService campaignImportService; + @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; private static final String CAMPAIGN_ID = "camp-1"; diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/ConfigControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/ConfigControllerTest.java index 6ae0c9a..63ac2c2 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/ConfigControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/ConfigControllerTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.Mockito.when; @@ -25,7 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. class ConfigControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private UpdateCheckService updates; + @MockitoBean private UpdateCheckService updates; @Test void getPublicConfig_returns200_updateCheckEnabledTrue() throws Exception { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/ConversationControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/ConversationControllerTest.java index 3e98cf0..f26e61c 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/ConversationControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/ConversationControllerTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @@ -41,7 +41,7 @@ class ConversationControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired private ConversationRepository conversationRepository; - @MockBean private ConversationTitleGenerator titleGenerator; + @MockitoBean private ConversationTitleGenerator titleGenerator; @Test void create_withLoreAnchor_returns200() throws Exception { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/GameSystemControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/GameSystemControllerTest.java index fe99a3f..b477b85 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/GameSystemControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/GameSystemControllerTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.core.task.TaskExecutor; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; @@ -52,8 +52,8 @@ class GameSystemControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; - @MockBean private RulesPdfImporter rulesPdfImporter; - @MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; + @MockitoBean private RulesPdfImporter rulesPdfImporter; + @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @BeforeEach void setUp() { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/ImageControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/ImageControllerTest.java index 2df5fb4..72fc917 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/ImageControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/ImageControllerTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; @@ -42,7 +42,7 @@ class ImageControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private ImageService imageService; + @MockitoBean private ImageService imageService; private Image sampleImage() { return Image.builder() diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/ItemCatalogControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/ItemCatalogControllerTest.java index 8ef3ef7..09f083d 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/ItemCatalogControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/ItemCatalogControllerTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @@ -49,7 +49,7 @@ class ItemCatalogControllerTest { @Autowired private ItemCatalogRepository catalogRepository; @Autowired private CampaignRepository campaignRepository; - @MockBean private ItemCatalogGenerator generator; + @MockitoBean private ItemCatalogGenerator generator; private String campaignId; diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/LicenseControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/LicenseControllerTest.java index 7899e7c..2209e9f 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/LicenseControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/LicenseControllerTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -54,8 +54,8 @@ class LicenseControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private LicenseService licenseService; - @MockBean private ChannelSwitcherService channelSwitcher; + @MockitoBean private LicenseService licenseService; + @MockitoBean private ChannelSwitcherService channelSwitcher; private LicenseSnapshot validSnapshot() { return new LicenseSnapshot( diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/NotebookControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/NotebookControllerTest.java index fd4ad51..25e9230 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/NotebookControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/NotebookControllerTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.core.task.TaskExecutor; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; @@ -59,9 +59,9 @@ class NotebookControllerTest { @Autowired private NotebookRepository notebookRepository; @Autowired private CampaignRepository campaignRepository; - @MockBean private NotebookIndexer indexer; - @MockBean private NotebookChatStreamer chatStreamer; - @MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; + @MockitoBean private NotebookIndexer indexer; + @MockitoBean private NotebookChatStreamer chatStreamer; + @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; private String campaignId; diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/PageGenerationControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/PageGenerationControllerTest.java index 51d8e7c..e1916e8 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/PageGenerationControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/PageGenerationControllerTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import java.util.Map; @@ -36,7 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. class PageGenerationControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private GeneratePageValuesUseCase generatePageValuesUseCase; + @MockitoBean private GeneratePageValuesUseCase generatePageValuesUseCase; @Test void generate_returns200_withSuggestions() throws Exception { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/RandomTableControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/RandomTableControllerTest.java index 122afe2..8d2960e 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/RandomTableControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/RandomTableControllerTest.java @@ -16,7 +16,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @@ -50,7 +50,7 @@ class RandomTableControllerTest { @Autowired private RandomTableRepository tableRepository; @Autowired private CampaignRepository campaignRepository; - @MockBean private RandomTableGenerator generator; + @MockitoBean private RandomTableGenerator generator; private String campaignId; diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerDemoModeTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerDemoModeTest.java index 1d8ad0b..90073be 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerDemoModeTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerDemoModeTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.HttpHeaders; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; @@ -36,7 +36,7 @@ class UpdatesControllerDemoModeTest { .encodeToString("test-admin:test-admin-password".getBytes(StandardCharsets.UTF_8)); @Autowired private MockMvc mockMvc; - @MockBean private UpdateCheckService updates; + @MockitoBean private UpdateCheckService updates; @Test void check_returns403_inDemoMode() throws Exception { diff --git a/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerTest.java b/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerTest.java index 1be83ba..a86faa0 100644 --- a/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerTest.java +++ b/core/src/test/java/com/loremind/infrastructure/web/controller/UpdatesControllerTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.HttpHeaders; import org.springframework.test.web.servlet.MockMvc; @@ -42,7 +42,7 @@ class UpdatesControllerTest { .encodeToString("test-admin:test-admin-password".getBytes(StandardCharsets.UTF_8)); @Autowired private MockMvc mockMvc; - @MockBean private UpdateCheckService updates; + @MockitoBean private UpdateCheckService updates; private UpdateStatus sampleUpdate() { return new UpdateStatus(true, true, false, "1.0.0", diff --git a/core/src/test/resources/application.properties b/core/src/test/resources/application.properties index 321c472..265007b 100644 --- a/core/src/test/resources/application.properties +++ b/core/src/test/resources/application.properties @@ -10,7 +10,6 @@ spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:loremind_test} spring.datasource.driver-class-name=org.postgresql.Driver # Configuration JPA pour les tests : schema recree a chaque run, pas de logs SQL. -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false @@ -19,7 +18,7 @@ spring.jpa.show-sql=false spring.flyway.enabled=false # Pool Hikari volontairement minuscule en test : la suite cree de nombreux -# contextes Spring distincts (combinaisons de @MockBean / @TestPropertySource), +# contextes Spring distincts (combinaisons de @MockitoBean / @TestPropertySource), # tous gardes en cache simultanement. Avec le pool par defaut (10), on epuisait # les connexions Postgres ("remaining connection slots are reserved"). 2 par # contexte suffit (tests sequentiels) et borne le total bien sous max_connections. @@ -40,3 +39,27 @@ minio.endpoint=http://localhost:9000 minio.access-key=test minio.secret-key=test minio.bucket=test-bucket + +# open-in-view fixe explicitement (defaut true) : supprime l'avertissement au boot. +spring.jpa.open-in-view=true + +# Le daemon de refresh de licence (@Scheduled) n'a rien a faire pendant les tests : +# son tick echouait en arriere-plan (table licenses droppee par le create-drop +# partage entre contextes) => stacktraces parasites. On le desactive ici. +licensing.refresh.enabled=false + +# --- Sortie de test LISIBLE --------------------------------------------------- +# Pas de banniere Spring (repetee a chaque contexte), et seuls les WARN/ERROR +# remontent : la masse d'INFO de demarrage (Spring/Hibernate/Hikari, "Started +# XxxTest", seeder...) n'apporte rien et noyait les vrais signaux. +spring.main.banner-mode=off +logging.level.root=WARN +# Tests de CHEMIN D'ERREUR : les controleurs loggent VOLONTAIREMENT l'echec (ex: +# Watchtower injoignable -> 502, Brain down -> 502) AVANT de renvoyer le bon statut +# HTTP teste. L'exception est attrapee, jamais propagee : ce sont des logs attendus. +logging.level.com.loremind.infrastructure.web.controller=OFF +# WARN attendus sur des tests de robustesse (registry injoignable, suppression best-effort). +logging.level.com.loremind.infrastructure.updates.UpdateCheckService=ERROR +logging.level.com.loremind.infrastructure.ai.BrainNotebookIndexClient=ERROR +# Le warning MinIO au @PostConstruct (serveur absent en test) est ATTENDU. +logging.level.com.loremind.infrastructure.storage=ERROR diff --git a/web/angular.json b/web/angular.json index afbedbe..32a08d1 100644 --- a/web/angular.json +++ b/web/angular.json @@ -52,7 +52,8 @@ "development": { "optimization": false, "sourceMap": true, - "namedChunks": true + "namedChunks": true, + "outputHashing": "none" } }, "defaultConfiguration": "production" diff --git a/web/e2e/tests/campaign/npc-edit.spec.ts b/web/e2e/tests/campaign/npc-edit.spec.ts index f0d57cf..afd56f5 100644 --- a/web/e2e/tests/campaign/npc-edit.spec.ts +++ b/web/e2e/tests/campaign/npc-edit.spec.ts @@ -35,7 +35,7 @@ test.describe('NPC edit', () => { await page.getByLabel(/Nom du PNJ/i).fill(newName); - await page.getByRole('button', { name: /^Enregistrer$/i }).click(); + await page.getByRole('button', { name: /^Sauvegarder$/i }).click(); // Retour à la campagne après save await expect(page).toHaveURL(new RegExp(`/campaigns/${campaign.id}$`)); @@ -48,7 +48,7 @@ test.describe('NPC edit', () => { await page.goto(`/campaigns/${campaign.id}/npcs/${npc.id}/edit`); const nameField = page.getByLabel(/Nom du PNJ/i); - const saveBtn = page.getByRole('button', { name: /^Enregistrer$/i }); + const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i }); await expect(saveBtn).toBeEnabled(); await nameField.fill(''); diff --git a/web/e2e/tests/game-system/game-system-edit.spec.ts b/web/e2e/tests/game-system/game-system-edit.spec.ts index a01adf4..fe2af0a 100644 --- a/web/e2e/tests/game-system/game-system-edit.spec.ts +++ b/web/e2e/tests/game-system/game-system-edit.spec.ts @@ -42,7 +42,7 @@ test.describe('GameSystem edit', () => { await page.getByLabel(/^Nom/i).fill(newName); await page.getByLabel(/Description courte/i).fill(newDescription); - await page.getByRole('button', { name: /^Enregistrer$/i }).click(); + await page.getByRole('button', { name: /^Sauvegarder$/i }).click(); // Retour a la liste apres save. await expect(page).toHaveURL(/\/game-systems$/); @@ -57,7 +57,7 @@ test.describe('GameSystem edit', () => { await expect(page.getByLabel(/^Nom/i)).toHaveValue(gs.name); const nameField = page.getByLabel(/^Nom/i); - const saveBtn = page.getByRole('button', { name: /^Enregistrer$/i }); + const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i }); await expect(saveBtn).toBeEnabled(); await nameField.fill(''); diff --git a/web/e2e/tests/game-system/game-system-sections.spec.ts b/web/e2e/tests/game-system/game-system-sections.spec.ts index 9bc47b0..70c2068 100644 --- a/web/e2e/tests/game-system/game-system-sections.spec.ts +++ b/web/e2e/tests/game-system/game-system-sections.spec.ts @@ -38,7 +38,7 @@ test.describe('GameSystem rule sections editor', () => { await card.locator('.section-content').fill(sectionContent); // Save + retour a la liste. - await page.getByRole('button', { name: /^Enregistrer$/i }).click(); + await page.getByRole('button', { name: /^Sauvegarder$/i }).click(); await expect(page).toHaveURL(/\/game-systems$/); // Verification cote API : le markdown contient bien la section + son contenu. diff --git a/web/e2e/tests/game-system/game-system-templates.spec.ts b/web/e2e/tests/game-system/game-system-templates.spec.ts index 823d29c..f07d7bb 100644 --- a/web/e2e/tests/game-system/game-system-templates.spec.ts +++ b/web/e2e/tests/game-system/game-system-templates.spec.ts @@ -44,7 +44,7 @@ test.describe('GameSystem template fields editor (PJ / PNJ)', () => { await expect(row.locator('.tfe-name')).toHaveValue('Histoire'); // Save → retour a la liste. - await page.getByRole('button', { name: /^Enregistrer$/i }).click(); + await page.getByRole('button', { name: /^Sauvegarder$/i }).click(); await expect(page).toHaveURL(/\/game-systems$/); // Verification API : le champ est bien dans characterTemplate. @@ -88,7 +88,7 @@ test.describe('GameSystem template fields editor (PJ / PNJ)', () => { await expect(tfe(page, 'PJ').locator('.tfe-item').first().locator('.tfe-name')).toHaveValue('Histoire'); await expect(tfe(page, 'PNJ').locator('.tfe-item').first().locator('.tfe-name')).toHaveValue('Motivation'); - await page.getByRole('button', { name: /^Enregistrer$/i }).click(); + await page.getByRole('button', { name: /^Sauvegarder$/i }).click(); await expect(page).toHaveURL(/\/game-systems$/); const persisted = await request.get(`/api/game-systems/${gs.id}`).then((r) => r.json()); diff --git a/web/package-lock.json b/web/package-lock.json index e08a3eb..e8c463b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "loremind-web", - "version": "0.16.2", + "version": "0.16.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loremind-web", - "version": "0.16.2", + "version": "0.16.3", "dependencies": { "@angular/animations": "^21.2.16", "@angular/common": "^21.2.16", diff --git a/web/package.json b/web/package.json index 36aaa41..eda8e21 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "loremind-web", - "version": "0.16.2", + "version": "0.16.3", "description": "LoreMind Frontend - Angular", "scripts": { "ng": "ng", diff --git a/web/playwright.config.ts b/web/playwright.config.ts index f29381c..d6bc06e 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -17,6 +17,12 @@ export default defineConfig({ reporter: process.env['CI'] ? [['html', { open: 'never' }], ['list']] : 'html', use: { baseURL, + // Locale FR épinglée : l'app résout sa langue via celle du navigateur + // (LanguageService.resolveInitialLang → getBrowserLang()). Sans ça, le + // Chromium de Playwright démarre en en-US → l'UI passe en anglais → tous les + // tests, écrits pour les libellés français, échouent (vert sur une machine en + // locale FR, rouge en CI Linux en-US). On fixe donc le français, déterministe. + locale: 'fr-FR', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', diff --git a/web/src/app/lore/page-edit/page-edit.component.html b/web/src/app/lore/page-edit/page-edit.component.html index 574ac67..b3d72ab 100644 --- a/web/src/app/lore/page-edit/page-edit.component.html +++ b/web/src/app/lore/page-edit/page-edit.component.html @@ -108,7 +108,7 @@ - @for (row of tableValues[field.name] ?? []; track $index; let ri = $index) { + @for (row of tableRows(field.name); track $index; let ri = $index) { @for (col of field.labels; track $index) { diff --git a/web/src/app/lore/page-edit/page-edit.component.ts b/web/src/app/lore/page-edit/page-edit.component.ts index 371f631..415db58 100644 --- a/web/src/app/lore/page-edit/page-edit.component.ts +++ b/web/src/app/lore/page-edit/page-edit.component.ts @@ -240,6 +240,11 @@ export class PageEditComponent implements OnInit, OnDestroy { this.tableValues[fieldName]?.splice(rowIndex, 1); } + /** Lignes du tableau d'un champ — toujours un tableau (jamais undefined) pour le `@for`. */ + tableRows(fieldName: string): Array> { + return this.tableValues[fieldName] ?? []; + } + // --- Chat IA conversationnel (Phase b5) -------------------------------- toggleChat(): void { diff --git a/web/src/app/services/quest-progression.service.ts b/web/src/app/services/quest-progression.service.ts deleted file mode 100644 index 35c2cb4..0000000 --- a/web/src/app/services/quest-progression.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { ProgressionStatus } from './campaign.model'; - -/** - * Endpoints de progression des quêtes pour un Playthrough. - * Modèle "absence = NOT_STARTED" — envoyer NOT_STARTED supprime la ligne côté backend. - */ -@Injectable({ providedIn: 'root' }) -export class QuestProgressionService { - - constructor(private http: HttpClient) {} - - /** Map chapterId -> ProgressionStatus pour le Playthrough donné. */ - list(playthroughId: string): Observable> { - return this.http.get>( - `/api/playthroughs/${playthroughId}/quest-progressions` - ); - } - - setStatus(playthroughId: string, chapterId: string, status: ProgressionStatus): Observable { - return this.http.put( - `/api/playthroughs/${playthroughId}/quest-progressions/${chapterId}`, - { status } - ); - } -} diff --git a/web/src/app/shared/dynamic-fields-form/dynamic-fields-form.component.html b/web/src/app/shared/dynamic-fields-form/dynamic-fields-form.component.html index 47209d7..9632d37 100644 --- a/web/src/app/shared/dynamic-fields-form/dynamic-fields-form.component.html +++ b/web/src/app/shared/dynamic-fields-form/dynamic-fields-form.component.html @@ -1,4 +1,4 @@ -@if (fields?.length) { +@if (fields.length) {
@for (f of fields; track trackByName($index, f)) {
diff --git a/web/src/app/shared/quest-status-badge/quest-status-badge.component.html b/web/src/app/shared/quest-status-badge/quest-status-badge.component.html deleted file mode 100644 index 302d972..0000000 --- a/web/src/app/shared/quest-status-badge/quest-status-badge.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - @if (!compact) { - {{ label }} - } - diff --git a/web/src/app/shared/quest-status-badge/quest-status-badge.component.scss b/web/src/app/shared/quest-status-badge/quest-status-badge.component.scss deleted file mode 100644 index b0fb777..0000000 --- a/web/src/app/shared/quest-status-badge/quest-status-badge.component.scss +++ /dev/null @@ -1,43 +0,0 @@ -.status-badge { - display: inline-flex; - align-items: center; - gap: 0.35rem; - padding: 0.2rem 0.55rem; - border-radius: 999px; - font-size: 0.78rem; - font-weight: 500; - line-height: 1; - border: 1px solid transparent; -} - -.status-label { - white-space: nowrap; -} - -// Verrouillée — gris discret -.status-locked { - background: rgba(128, 128, 128, 0.12); - color: #6b6b6b; - border-color: rgba(128, 128, 128, 0.25); -} - -// Disponible — vert calme (prête à être lancée) -.status-available { - background: rgba(52, 168, 83, 0.12); - color: #2f7a47; - border-color: rgba(52, 168, 83, 0.3); -} - -// En cours — bleu actif -.status-in_progress { - background: rgba(66, 133, 244, 0.14); - color: #2c6cd6; - border-color: rgba(66, 133, 244, 0.35); -} - -// Terminée — violet doux (clos, pas neutre) -.status-completed { - background: rgba(120, 80, 200, 0.12); - color: #6d4fb5; - border-color: rgba(120, 80, 200, 0.3); -} diff --git a/web/src/app/shared/quest-status-badge/quest-status-badge.component.ts b/web/src/app/shared/quest-status-badge/quest-status-badge.component.ts deleted file mode 100644 index 90ca34a..0000000 --- a/web/src/app/shared/quest-status-badge/quest-status-badge.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, Input } from '@angular/core'; - -import { LucideAngularModule, Lock, Circle, Play, CheckCircle2, LucideIconData } from 'lucide-angular'; -import { TranslateService } from '@ngx-translate/core'; -import { QuestStatus } from '../../services/campaign.model'; - -/** - * Badge visuel pour un QuestStatus (vue Hub). - * Composant standalone, sans dépendance métier. - */ -@Component({ - selector: 'app-quest-status-badge', - imports: [LucideAngularModule], - templateUrl: './quest-status-badge.component.html', - styleUrls: ['./quest-status-badge.component.scss'] -}) -export class QuestStatusBadgeComponent { - @Input() status: QuestStatus | undefined | null = 'AVAILABLE'; - - /** Variante visuelle compacte (sans label) — utile pour les listes denses. */ - @Input() compact = false; - - constructor(private translate: TranslateService) {} - - get icon(): LucideIconData { - switch (this.status) { - case 'LOCKED': return Lock; - case 'IN_PROGRESS': return Play; - case 'COMPLETED': return CheckCircle2; - case 'AVAILABLE': - default: return Circle; - } - } - - get label(): string { - switch (this.status) { - case 'LOCKED': return this.translate.instant('questStatusBadge.locked'); - case 'IN_PROGRESS': return this.translate.instant('questStatusBadge.inProgress'); - case 'COMPLETED': return this.translate.instant('questStatusBadge.completed'); - case 'AVAILABLE': - default: return this.translate.instant('questStatusBadge.available'); - } - } - - get cssClass(): string { - return `status-badge status-${(this.status ?? 'AVAILABLE').toLowerCase()}`; - } -}