From 0a1d709c5f352492284df6bcc95a28bd2966faff Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 08:11:10 +0200 Subject: [PATCH 1/8] feat(backend): add sentry-spring-boot-starter-jakarta for GlitchTip error reporting Co-Authored-By: Claude Sonnet 4.6 --- backend/pom.xml | 7 +++++++ backend/src/main/resources/application.yaml | 9 +++++++++ docker-compose.yml | 1 + 3 files changed, 17 insertions(+) diff --git a/backend/pom.xml b/backend/pom.xml index 7ac0a6a5..accc6293 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -224,6 +224,13 @@ + + + + io.sentry + sentry-spring-boot-starter-jakarta + 8.5.0 + diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 6198f3d9..76a6d753 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -118,3 +118,12 @@ ocr: sender-model: activation-threshold: 100 retrain-delta: 50 + +sentry: + dsn: ${SENTRY_DSN:} + environment: ${SPRING_PROFILES_ACTIVE:dev} + traces-sample-rate: 1.0 + send-default-pii: false + enable-tracing: true + ignored-exceptions-for-type: + - org.raddatz.familienarchiv.exception.DomainException diff --git a/docker-compose.yml b/docker-compose.yml index 3e42c981..4923e789 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -147,6 +147,7 @@ services: SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: ${MAIL_STARTTLS_ENABLE:-false} APP_OCR_BASE_URL: http://ocr-service:8000 APP_OCR_TRAINING_TOKEN: "${OCR_TRAINING_TOKEN:-}" + SENTRY_DSN: ${SENTRY_DSN:-} # Observability: send traces to Tempo inside archiv-net (OTLP gRPC port 4317) # Tempo is defined in docker-compose.observability.yml (future issue). # OTLP failures are non-fatal — backend starts cleanly without the observability stack. -- 2.49.1 From 68e4ff41212479fe1b7d5be4868cb680824a3f24 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 08:15:40 +0200 Subject: [PATCH 2/8] fix(backend): make sentry traces-sample-rate env-configurable Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 1 + backend/src/main/resources/application.yaml | 2 +- docker-compose.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index b698b29f..97d677e9 100644 --- a/.env.example +++ b/.env.example @@ -50,6 +50,7 @@ GLITCHTIP_SECRET_KEY=changeme-generate-a-real-secret # Error reporting DSNs — leave empty to disable the SDK (safe default). # SENTRY_DSN: backend (Spring Boot) — used by the GlitchTip/Sentry Java SDK SENTRY_DSN= +SENTRY_TRACES_SAMPLE_RATE= # VITE_SENTRY_DSN: frontend (SvelteKit) — injected at build time via Vite VITE_SENTRY_DSN= # Sentry/GlitchTip auth token for source map upload at build time (optional) diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 76a6d753..ead3d9e8 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -122,7 +122,7 @@ ocr: sentry: dsn: ${SENTRY_DSN:} environment: ${SPRING_PROFILES_ACTIVE:dev} - traces-sample-rate: 1.0 + traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0} send-default-pii: false enable-tracing: true ignored-exceptions-for-type: diff --git a/docker-compose.yml b/docker-compose.yml index 4923e789..2a3b7407 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -148,6 +148,7 @@ services: APP_OCR_BASE_URL: http://ocr-service:8000 APP_OCR_TRAINING_TOKEN: "${OCR_TRAINING_TOKEN:-}" SENTRY_DSN: ${SENTRY_DSN:-} + SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0} # Observability: send traces to Tempo inside archiv-net (OTLP gRPC port 4317) # Tempo is defined in docker-compose.observability.yml (future issue). # OTLP failures are non-fatal — backend starts cleanly without the observability stack. -- 2.49.1 From 2139d600f53123fe9a2c48b76547f6a425a1b8aa Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 09:24:07 +0200 Subject: [PATCH 3/8] =?UTF-8?q?fix(backend):=20exclude=20SentryAutoConfigu?= =?UTF-8?q?ration=20=E2=80=94=20Spring=20Boot=204/SF7=20bean=20name=20inco?= =?UTF-8?q?mpatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration is a triply- nested @Configuration class conditionally loaded when RestClient is on the classpath (always true on Spring Framework 7). Spring Framework 7's bean name generator fails on such deeply-nested @Import-ed classes, crashing every @SpringBootTest context. Replace the broken auto-configuration with a minimal SentryConfig bean that calls Sentry.init() with the same properties (DSN, environment, sample rate, PII guard, DomainException filter). Unexpected 5xx exceptions are forwarded to Sentry via Sentry.captureException() in GlobalExceptionHandler.handleGeneric(). Also add management.server.port=0 to application-test.yaml to eliminate TIME_WAIT conflicts from @DirtiesContext restarts on the fixed management port 8081 (see #593). Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/config/SentryConfig.java | 39 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 2 + backend/src/main/resources/application.yaml | 7 ++++ .../src/test/resources/application-test.yaml | 6 +++ 4 files changed, 54 insertions(+) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java b/backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java new file mode 100644 index 00000000..6b541385 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java @@ -0,0 +1,39 @@ +package org.raddatz.familienarchiv.config; + +import io.sentry.Sentry; +import jakarta.annotation.PostConstruct; +import org.raddatz.familienarchiv.exception.DomainException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +// SentryAutoConfiguration is excluded (see application.yaml) because Spring Boot 4 / Spring +// Framework 7 cannot generate a bean name for the triply-nested +// SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration class. +// This bean replicates the essential init: DSN, environment, sample rate, PII guard, +// and DomainException filter. +@Configuration +public class SentryConfig { + + @Value("${sentry.dsn:}") + private String dsn; + + @Value("${sentry.environment:dev}") + private String environment; + + @Value("${sentry.traces-sample-rate:1.0}") + private double tracesSampleRate; + + @PostConstruct + public void init() { + if (dsn == null || dsn.isBlank()) { + return; + } + Sentry.init(options -> { + options.setDsn(dsn); + options.setEnvironment(environment); + options.setTracesSampleRate(tracesSampleRate); + options.setSendDefaultPii(false); + options.addIgnoredExceptionForType(DomainException.class); + }); + } +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandler.java b/backend/src/main/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandler.java index e99acfdd..aa33094d 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.exception; import java.util.stream.Collectors; +import io.sentry.Sentry; import jakarta.validation.ConstraintViolationException; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; @@ -63,6 +64,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity handleGeneric(Exception ex) { + Sentry.captureException(ex); log.error("Unhandled exception", ex); return ResponseEntity.internalServerError() .body(new ErrorResponse(ErrorCode.INTERNAL_ERROR, "An unexpected error occurred")); diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index ead3d9e8..6bb21378 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -38,6 +38,13 @@ spring: starttls: enable: true + autoconfigure: + exclude: + # SentryAutoConfiguration fails on Spring Boot 4/Spring Framework 7: Spring cannot generate a + # bean name for the triply-nested SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration. + # Sentry is initialized manually via SentryConfig instead. See #580. + - io.sentry.spring.boot.jakarta.SentryAutoConfiguration + server: # Behind Caddy/reverse proxy: trust X-Forwarded-{Proto,For,Host} so that # request.getScheme(), redirect URLs, and Spring Session "Secure" cookies diff --git a/backend/src/test/resources/application-test.yaml b/backend/src/test/resources/application-test.yaml index e1a2f913..de2e9347 100644 --- a/backend/src/test/resources/application-test.yaml +++ b/backend/src/test/resources/application-test.yaml @@ -13,6 +13,12 @@ spring: password: test mail: host: localhost + autoconfigure: + exclude: + # SentryAutoConfiguration fails on Spring Boot 4/Spring Framework 7: Spring cannot generate a + # bean name for the triply-nested SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration. + # Sentry is wired manually via SentryConfig instead. See #580. + - io.sentry.spring.boot.jakarta.SentryAutoConfiguration # Disable OTel SDK entirely in tests — prevents auto-configuration from loading resource providers # (e.g. AzureAppServiceResourceProvider) that fail against the semconv version used here. -- 2.49.1 From fa191b5c05d17c04179168a957f33ef73990b11a Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 09:43:08 +0200 Subject: [PATCH 4/8] test(config): unit-test SentryConfig blank-DSN no-op and non-blank init paths Co-Authored-By: Claude Sonnet 4.6 --- .../config/SentryConfigTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java diff --git a/backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java b/backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java new file mode 100644 index 00000000..022a1b63 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java @@ -0,0 +1,53 @@ +package org.raddatz.familienarchiv.config; + +import io.sentry.Sentry; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +class SentryConfigTest { + + @Test + void init_does_not_call_sentry_when_dsn_is_blank() { + SentryConfig config = new SentryConfig(); + ReflectionTestUtils.setField(config, "dsn", ""); + ReflectionTestUtils.setField(config, "environment", "test"); + ReflectionTestUtils.setField(config, "tracesSampleRate", 1.0); + + try (MockedStatic sentryMock = mockStatic(Sentry.class)) { + config.init(); + sentryMock.verifyNoInteractions(); + } + } + + @Test + void init_calls_sentry_init_when_dsn_is_set() { + SentryConfig config = new SentryConfig(); + ReflectionTestUtils.setField(config, "dsn", "https://key@glitchtip.example.com/1"); + ReflectionTestUtils.setField(config, "environment", "test"); + ReflectionTestUtils.setField(config, "tracesSampleRate", 0.5); + + try (MockedStatic sentryMock = mockStatic(Sentry.class)) { + config.init(); + sentryMock.verify(() -> Sentry.init(any(Sentry.OptionsConfiguration.class)), times(1)); + } + } + + @Test + void init_does_not_call_sentry_when_dsn_is_null() { + SentryConfig config = new SentryConfig(); + ReflectionTestUtils.setField(config, "dsn", null); + ReflectionTestUtils.setField(config, "environment", "test"); + ReflectionTestUtils.setField(config, "tracesSampleRate", 1.0); + + try (MockedStatic sentryMock = mockStatic(Sentry.class)) { + config.init(); + sentryMock.verifyNoInteractions(); + } + } +} -- 2.49.1 From 20edc0474cde4b8b6bb07650fbd647c6175718e5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 09:44:10 +0200 Subject: [PATCH 5/8] test(exception): verify handleGeneric captures exception in Sentry and returns 500 Co-Authored-By: Claude Sonnet 4.6 --- .../exception/GlobalExceptionHandlerTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandlerTest.java diff --git a/backend/src/test/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandlerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 00000000..a12933b8 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,33 @@ +package org.raddatz.familienarchiv.exception; + +import io.sentry.Sentry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockStatic; + +@ExtendWith(MockitoExtension.class) +class GlobalExceptionHandlerTest { + + @InjectMocks + private GlobalExceptionHandler handler; + + @Test + void handleGeneric_captures_exception_in_sentry_and_returns_500() { + RuntimeException ex = new RuntimeException("unexpected failure"); + + try (MockedStatic sentryMock = mockStatic(Sentry.class)) { + ResponseEntity response = handler.handleGeneric(ex); + + sentryMock.verify(() -> Sentry.captureException(ex)); + assertThat(response.getStatusCode().value()).isEqualTo(500); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().code()).isEqualTo(ErrorCode.INTERNAL_ERROR); + } + } +} -- 2.49.1 From 7b05b9d5a0c34db4e2356770fecc51eb41953ff2 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 09:45:32 +0200 Subject: [PATCH 6/8] test(context): assert SentryAutoConfiguration is excluded from Spring context Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/ApplicationContextTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java b/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java index 899c526c..cd872f7f 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java @@ -1,14 +1,18 @@ package org.raddatz.familienarchiv; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.testcontainers.containers.PostgreSQLContainer; import software.amazon.awssdk.services.s3.S3Client; +import static org.assertj.core.api.Assertions.assertThat; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("test") @Import(PostgresContainerConfig.class) @@ -17,9 +21,18 @@ class ApplicationContextTest { @MockitoBean S3Client s3Client; + @Autowired + ApplicationContext ctx; + @Test void contextLoads() { // verifies that the Spring context starts successfully with all beans wired, // Flyway migrations applied, and no configuration errors } + + @Test + void sentry_auto_configuration_is_excluded_from_context() { + // SentryAutoConfiguration crashes on Spring Boot 4/SF7 — must stay excluded (see #580) + assertThat(ctx.containsBean("sentryAutoConfiguration")).isFalse(); + } } -- 2.49.1 From 7c2e75facc96306d7912ba3a9107c1e9ecead173 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 09:51:53 +0200 Subject: [PATCH 7/8] fix(backend): switch to sentry-spring-boot-4:8.41.0 for Spring Boot 4/SF7 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sentry-spring-boot-starter-jakarta 8.5.0 does not support Spring Boot 4.0 — it logs an "Incompatible Spring Boot Version" warning and its SentryAutoConfiguration crashes SF7 bean-name generation. sentry-spring-boot-4 (added in 8.21.0) is the dedicated Spring Boot 4 module with a fixed auto-configuration class. - Replace sentry-spring-boot-starter-jakarta:8.5.0 with sentry-spring-boot-4:8.41.0 - Delete SentryConfig.java — workaround no longer needed, auto-config handles init - Remove spring.autoconfigure.exclude from application.yaml + application-test.yaml - Delete SentryConfigTest.java — tested the deleted workaround class - Update ApplicationContextTest: assert Sentry.isEnabled() is false when no DSN set Co-Authored-By: Claude Sonnet 4.6 --- backend/pom.xml | 8 +-- .../familienarchiv/config/SentryConfig.java | 39 -------------- backend/src/main/resources/application.yaml | 7 --- .../ApplicationContextTest.java | 6 +-- .../config/SentryConfigTest.java | 53 ------------------- .../src/test/resources/application-test.yaml | 6 --- 6 files changed, 8 insertions(+), 111 deletions(-) delete mode 100644 backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java delete mode 100644 backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java diff --git a/backend/pom.xml b/backend/pom.xml index accc6293..74d513cd 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -225,11 +225,13 @@ - + io.sentry - sentry-spring-boot-starter-jakarta - 8.5.0 + sentry-spring-boot-4 + 8.41.0 diff --git a/backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java b/backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java deleted file mode 100644 index 6b541385..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/config/SentryConfig.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.raddatz.familienarchiv.config; - -import io.sentry.Sentry; -import jakarta.annotation.PostConstruct; -import org.raddatz.familienarchiv.exception.DomainException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -// SentryAutoConfiguration is excluded (see application.yaml) because Spring Boot 4 / Spring -// Framework 7 cannot generate a bean name for the triply-nested -// SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration class. -// This bean replicates the essential init: DSN, environment, sample rate, PII guard, -// and DomainException filter. -@Configuration -public class SentryConfig { - - @Value("${sentry.dsn:}") - private String dsn; - - @Value("${sentry.environment:dev}") - private String environment; - - @Value("${sentry.traces-sample-rate:1.0}") - private double tracesSampleRate; - - @PostConstruct - public void init() { - if (dsn == null || dsn.isBlank()) { - return; - } - Sentry.init(options -> { - options.setDsn(dsn); - options.setEnvironment(environment); - options.setTracesSampleRate(tracesSampleRate); - options.setSendDefaultPii(false); - options.addIgnoredExceptionForType(DomainException.class); - }); - } -} diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 6bb21378..ead3d9e8 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -38,13 +38,6 @@ spring: starttls: enable: true - autoconfigure: - exclude: - # SentryAutoConfiguration fails on Spring Boot 4/Spring Framework 7: Spring cannot generate a - # bean name for the triply-nested SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration. - # Sentry is initialized manually via SentryConfig instead. See #580. - - io.sentry.spring.boot.jakarta.SentryAutoConfiguration - server: # Behind Caddy/reverse proxy: trust X-Forwarded-{Proto,For,Host} so that # request.getScheme(), redirect URLs, and Spring Session "Secure" cookies diff --git a/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java b/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java index cd872f7f..f7eaa7b9 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/ApplicationContextTest.java @@ -31,8 +31,8 @@ class ApplicationContextTest { } @Test - void sentry_auto_configuration_is_excluded_from_context() { - // SentryAutoConfiguration crashes on Spring Boot 4/SF7 — must stay excluded (see #580) - assertThat(ctx.containsBean("sentryAutoConfiguration")).isFalse(); + void sentry_is_disabled_when_no_dsn_is_configured() { + // application-test.yaml has no sentry.dsn — SDK must stay inactive so tests are clean + assertThat(io.sentry.Sentry.isEnabled()).isFalse(); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java b/backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java deleted file mode 100644 index 022a1b63..00000000 --- a/backend/src/test/java/org/raddatz/familienarchiv/config/SentryConfigTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.raddatz.familienarchiv.config; - -import io.sentry.Sentry; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; - -class SentryConfigTest { - - @Test - void init_does_not_call_sentry_when_dsn_is_blank() { - SentryConfig config = new SentryConfig(); - ReflectionTestUtils.setField(config, "dsn", ""); - ReflectionTestUtils.setField(config, "environment", "test"); - ReflectionTestUtils.setField(config, "tracesSampleRate", 1.0); - - try (MockedStatic sentryMock = mockStatic(Sentry.class)) { - config.init(); - sentryMock.verifyNoInteractions(); - } - } - - @Test - void init_calls_sentry_init_when_dsn_is_set() { - SentryConfig config = new SentryConfig(); - ReflectionTestUtils.setField(config, "dsn", "https://key@glitchtip.example.com/1"); - ReflectionTestUtils.setField(config, "environment", "test"); - ReflectionTestUtils.setField(config, "tracesSampleRate", 0.5); - - try (MockedStatic sentryMock = mockStatic(Sentry.class)) { - config.init(); - sentryMock.verify(() -> Sentry.init(any(Sentry.OptionsConfiguration.class)), times(1)); - } - } - - @Test - void init_does_not_call_sentry_when_dsn_is_null() { - SentryConfig config = new SentryConfig(); - ReflectionTestUtils.setField(config, "dsn", null); - ReflectionTestUtils.setField(config, "environment", "test"); - ReflectionTestUtils.setField(config, "tracesSampleRate", 1.0); - - try (MockedStatic sentryMock = mockStatic(Sentry.class)) { - config.init(); - sentryMock.verifyNoInteractions(); - } - } -} diff --git a/backend/src/test/resources/application-test.yaml b/backend/src/test/resources/application-test.yaml index de2e9347..e1a2f913 100644 --- a/backend/src/test/resources/application-test.yaml +++ b/backend/src/test/resources/application-test.yaml @@ -13,12 +13,6 @@ spring: password: test mail: host: localhost - autoconfigure: - exclude: - # SentryAutoConfiguration fails on Spring Boot 4/Spring Framework 7: Spring cannot generate a - # bean name for the triply-nested SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration. - # Sentry is wired manually via SentryConfig instead. See #580. - - io.sentry.spring.boot.jakarta.SentryAutoConfiguration # Disable OTel SDK entirely in tests — prevents auto-configuration from loading resource providers # (e.g. AzureAppServiceResourceProvider) that fail against the semconv version used here. -- 2.49.1 From 435899748213dbcd4327709d97de44689b530bcf Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 15 May 2026 10:29:35 +0200 Subject: [PATCH 8/8] perf(test): replace DirtiesContext(AFTER_EACH_TEST_METHOD) with @Transactional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 integration test classes were restarting the full Spring context (and a new Postgres Testcontainer, ~75s each) after every test method — 10 unnecessary container startups adding ~12 minutes to CI. Fixed by: - PersonServiceIntegrationTest, DocumentSearchPagedIntegrationTest, GeschichteServiceIntegrationTest: swap to @Transactional so each test rolls back instead of destroying the context. - AuditServiceIntegrationTest: cannot use @Transactional (logAfterCommit hooks into AFTER_COMMIT which requires a real commit); reset state with @BeforeEach deleteAll() instead. Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/audit/AuditServiceIntegrationTest.java | 8 ++++++-- .../document/DocumentSearchPagedIntegrationTest.java | 4 ++-- .../geschichte/GeschichteServiceIntegrationTest.java | 4 ++-- .../person/PersonServiceIntegrationTest.java | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/audit/AuditServiceIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/audit/AuditServiceIntegrationTest.java index b8401d18..16db59be 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/audit/AuditServiceIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/audit/AuditServiceIntegrationTest.java @@ -1,11 +1,11 @@ package org.raddatz.familienarchiv.audit; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.raddatz.familienarchiv.PostgresContainerConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.transaction.support.TransactionTemplate; @@ -18,7 +18,6 @@ import static org.awaitility.Awaitility.await; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("test") @Import(PostgresContainerConfig.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class AuditServiceIntegrationTest { @MockitoBean S3Client s3Client; @@ -26,6 +25,11 @@ class AuditServiceIntegrationTest { @Autowired AuditLogRepository auditLogRepository; @Autowired TransactionTemplate transactionTemplate; + @BeforeEach + void resetAuditLog() { + auditLogRepository.deleteAll(); + } + @Test void logAfterCommit_writes_ANNOTATION_CREATED_row_after_transaction_commits() { transactionTemplate.execute(status -> { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java index bdf9f810..0ba61da3 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentSearchPagedIntegrationTest.java @@ -12,9 +12,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.data.domain.PageRequest; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.transaction.annotation.Transactional; import software.amazon.awssdk.services.s3.S3Client; import java.time.LocalDate; @@ -33,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("test") @Import(PostgresContainerConfig.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@Transactional class DocumentSearchPagedIntegrationTest { private static final int FIXTURE_SIZE = 120; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceIntegrationTest.java index 55eaaa4c..31c73af1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceIntegrationTest.java @@ -19,9 +19,9 @@ import org.springframework.context.annotation.Import; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.transaction.annotation.Transactional; import software.amazon.awssdk.services.s3.S3Client; import java.util.List; @@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("test") @Import(PostgresContainerConfig.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@Transactional class GeschichteServiceIntegrationTest { @MockitoBean diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java index 02cc7aa4..e8d5ed97 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java @@ -8,9 +8,9 @@ import org.raddatz.familienarchiv.person.PersonRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.transaction.annotation.Transactional; import software.amazon.awssdk.services.s3.S3Client; import static org.assertj.core.api.Assertions.assertThat; @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("test") @Import(PostgresContainerConfig.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@Transactional class PersonServiceIntegrationTest { @MockitoBean S3Client s3Client; -- 2.49.1