upgrade Java-25 Kotlin-2.3.0 usw.

This commit is contained in:
2025-12-30 16:08:40 +01:00
parent 84c3cfd787
commit 9283f26df1
31 changed files with 745 additions and 550 deletions
@@ -1,17 +1,12 @@
package at.mocode.infrastructure.gateway.config
import org.slf4j.LoggerFactory
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.GlobalFilter
import org.springframework.core.Ordered
import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import java.util.concurrent.ConcurrentHashMap
import java.util.UUID
import java.util.*
/**
* Gateway-Konfiguration für erweiterte Funktionalitäten wie Logging, Rate Limiting und Security.
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import reactor.core.publisher.Mono
@@ -14,6 +15,7 @@ class RateLimiterConfig {
* Funktioniert out-of-the-box mit Keycloak (Resource Server), sofern Security aktiv ist.
*/
@Bean
@ConditionalOnProperty(prefix = "gateway.ratelimit.principal-key-resolver", name = ["enabled"], havingValue = "true", matchIfMissing = false)
fun principalNameKeyResolver(): KeyResolver = KeyResolver { exchange ->
exchange.getPrincipal<Principal>()
.map { it.name }
@@ -0,0 +1,26 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
import org.springframework.context.annotation.Primary
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import reactor.core.publisher.Mono
@Configuration
class RateLimitingConfig {
/**
* Einfache IP-basierte KeyResolver-Implementierung für das RequestRateLimiter-Filter.
* Nutzt X-Forwarded-For, wenn vorhanden, sonst die Remote-Adresse.
*/
@Bean
@Primary
fun ipAddressKeyResolver(): KeyResolver = KeyResolver { exchange ->
val forwardedFor = exchange.request.headers.getFirst("X-Forwarded-For")
?.split(',')?.firstOrNull()?.trim()
val ip = forwardedFor
?: exchange.request.remoteAddress?.address?.hostAddress
?: "unknown"
Mono.just(ip)
}
}
@@ -0,0 +1,38 @@
package at.mocode.infrastructure.gateway.error
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
/**
* Einfacher ProblemDetails-Handler für unerwartete Fehler im Gateway.
* Gibt application/problem+json zurück mit Correlation-ID als traceId.
*/
@Component
class ProblemDetailsExceptionHandler : ErrorWebExceptionHandler {
private val mapper = ObjectMapper()
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
// Versuche, Status aus Attributen zu lesen, ansonsten 500
val status = exchange.response.statusCode?.value() ?: HttpStatus.INTERNAL_SERVER_ERROR.value()
val traceId = exchange.request.headers.getFirst("X-Correlation-ID")
val body = mapOf(
"type" to "about:blank",
"title" to (ex.message ?: "Unexpected error"),
"status" to status,
"traceId" to traceId
)
exchange.response.statusCode = HttpStatus.valueOf(status)
exchange.response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON
val bytes = mapper.writeValueAsBytes(body)
val buffer = exchange.response.bufferFactory().wrap(bytes)
return exchange.response.writeWith(Mono.just(buffer))
}
}
@@ -0,0 +1,27 @@
package at.mocode.infrastructure.gateway.fallback
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.time.Instant
/**
* Alternative FallbackController (deaktiviert per Default), nur aktivierbar über
* property `gateway.customFallback.enabled=true`. Standardmäßig existiert bereits
* ein FallbackController unter `...gateway.controller.FallbackController`.
*/
@RestController
@ConditionalOnProperty(prefix = "gateway.customFallback", name = ["enabled"], havingValue = "true", matchIfMissing = false)
class FallbackController {
@RequestMapping("/fallback/ping")
fun pingFallback(): ResponseEntity<Map<String, Any>> =
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(
mapOf(
"message" to "Ping service unavailable",
"timestamp" to Instant.now().toString()
)
)
}
@@ -46,13 +46,18 @@ class SecurityConfig(
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
permitAll
)
// Ping-API erfordert Admin-Rolle (Realm-Rolle "admin")
authorize(pathMatchers("/api/ping/**"), hasRole("admin"))
// Alle anderen Pfade erfordern eine Authentifizierung
authorize(anyExchange, authenticated)
}
// 4. JWT-Validierung via Keycloak aktivieren
oauth2ResourceServer {
jwt { }
jwt {
// Realm-Rollen (Keycloak) -> ROLE_* Authorities
jwtAuthenticationConverter = realmRolesJwtAuthenticationConverter()
}
}
}
}
@@ -93,6 +98,22 @@ class SecurityConfig(
}
}
/**
* Konvertiert Keycloak Realm-Rollen (realm_access.roles) in Spring Authorities (ROLE_*),
* sodass hasRole("admin") funktioniert.
*/
@Bean
fun realmRolesJwtAuthenticationConverter(): org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter {
val converter = org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
val roles = (jwt.claims["realm_access"] as? Map<*, *>)?.get("roles") as? Collection<*> ?: emptyList<Any>()
roles
.filterIsInstance<String>()
.map { role -> org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + role.lowercase()) }
}
return org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter(converter)
}
/**
* Definiert die zentrale und einzige CORS-Konfiguration für das Gateway.
*/
@@ -125,8 +146,7 @@ data class GatewaySecurityProperties(
"/actuator/**",
"/webjars/**",
"/v3/api-docs/**",
"/api/auth/**", // Alle Auth-Endpunkte
"/api/ping/**"
"/api/auth/**" // Alle Auth-Endpunkte
)
)
@@ -1,23 +1,13 @@
# ===================================================================
# Keycloak Profile Configuration
# ===================================================================
# This profile configures OAuth2/JWT authentication with Keycloak.
# Uses Spring Security's oauth2ResourceServer for secure JWT validation.
# ===================================================================
# migrated from application-keycloak.yml (standardized to .yaml)
spring:
security:
oauth2:
resourceserver:
jwt:
# Issuer URI for JWT validation - Docker internal: keycloak:8080, External: localhost:8180
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8180/realms/meldestelle}
# JWK Set URI for fetching public keys to validate JWT signatures
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8180/realms/meldestelle/protocol/openid-connect/certs}
# Keycloak-spezifische Konfiguration
keycloak:
# Internal Docker service name, external via port 8180
server-url: ${KEYCLOAK_SERVER_URL:http://keycloak:8180}
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8180/realms/meldestelle}
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8180/realms/meldestelle/protocol/openid-connect/certs}
@@ -0,0 +1,35 @@
spring:
application:
name: gateway
autoconfigure:
exclude:
- org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 5s
routes:
- id: ping-service
uri: http://ping-service:8080
predicates:
- Path=/api/ping/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: pingServiceCB
fallbackUri: forward:/fallback/ping
management:
endpoints:
web:
exposure:
include: health,info,prometheus
# Gateway-spezifische Einstellungen
gateway:
ratelimit:
enabled: false # Start: ausgeschaltet; zum Aktivieren default-filters plus RequestRateLimiter in YAML hinzufügen
replenish-rate: 10
burst-capacity: 20
@@ -1,321 +0,0 @@
# Port, auf dem das Gateway läuft
server:
port: ${GATEWAY_SERVER_PORT:8081}
# Optimierte Netty-Konfiguration für reaktive Anwendungen
netty:
connection-timeout: 5s
idle-timeout: 15s
# Der Name, unter dem sich das Gateway in Consul registriert
spring:
application:
name: api-gateway
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
security:
user:
name: ${GATEWAY_ADMIN_USER:admin}
password: ${GATEWAY_ADMIN_PASSWORD:admin}
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
timeout: 3s
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
enabled: ${CONSUL_ENABLED:true}
discovery:
enabled: ${CONSUL_ENABLED:true}
register: ${CONSUL_ENABLED:true}
health-check-path: /actuator/health
health-check-interval: 10s
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
gateway:
server:
webflux:
httpclient:
connect-timeout: 5000
response-timeout: 30s
pool:
max-idle-time: 15s
max-life-time: 60s
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- name: Retry
args:
retries: 2
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
methods: GET
backoff:
firstBackoff: 100ms
maxBackoff: 1000ms
factor: 2
basedOnPreviousValue: false
- name: AddResponseHeader
args:
name: X-Content-Type-Options
value: nosniff
- name: AddResponseHeader
args:
name: X-Frame-Options
value: DENY
- name: AddResponseHeader
args:
name: Referrer-Policy
value: strict-origin-when-cross-origin
- name: AddResponseHeader
args:
name: Cache-Control
value: no-cache, no-store, must-revalidate
routes:
# ==============================================================
# --- Gateway-Info-Route (optional) ---
# ==============================================================
- id: gateway-info-route
uri: http://localhost:${server.port}
predicates:
- Method=GET
- Path=/gateway-info
filters:
- name: SetStatus
args:
status: 200
- name: SetResponseHeader
args:
name: Content-Type
value: application/json
# ==============================================================
# --- Ping-Service-Integration (optional) ---
# ==============================================================
- id: ping-service-route
uri: lb://ping-service
predicates:
- Path=/api/ping/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: pingCircuitBreaker
fallbackUri: forward:/fallback/ping
- name: RequestRateLimiter
args:
key-resolver: "#{@principalNameKeyResolver}"
redis-rate-limiter.replenishRate: ${PING_RATE_LIMIT_REPLENISH_RATE:50}
redis-rate-limiter.burstCapacity: ${PING_RATE_LIMIT_BURST:100}
# ==============================================================
# --- Entries-Service-Integration (MP-27) ---
# ==============================================================
- id: entries-service-route
uri: lb://entries-service
predicates:
- Path=/api/entries/**
filters:
- StripPrefix=1
# Mappe das Root "/api/entries" explizit auf die Service-Root "/"
- id: entries-service-root
uri: lb://entries-service
predicates:
- Path=/api/entries
filters:
- SetPath=/
- id: entries-service-root-slash
uri: lb://entries-service
predicates:
- Path=/api/entries/
filters:
- SetPath=/
# ==============================================================
# --- Members-Service-Integration (optional) ---
# ==============================================================
# - id: members-service-route
# uri: lb://members-service
# predicates:
# - Path=/api/members/**
# filters:
# - StripPrefix=1
# - name: CircuitBreaker
# args:
# name: membersCircuitBreaker
# fallbackUri: forward:/fallback/members
# ==============================================================
# --- Horses-Service-Integration (optional) ---
# ==============================================================
# - id: horses-service-route
# uri: lb://horses-service
# predicates:
# - Path=/api/horses/**
# filters:
# - StripPrefix=1
# - name: CircuitBreaker
# args:
# name: horsesCircuitBreaker
# fallbackUri: forward:/fallback/horses
# ==============================================================
# --- Events-Service-Integration (optional) ---
# ==============================================================
# - id: events-service-route
# uri: lb://events-service
# predicates:
# - Path=/api/events/**
# filters:
# - StripPrefix=1
# - name: CircuitBreaker
# args:
# name: eventsCircuitBreaker
# fallbackUri: forward:/fallback/events
# ==============================================================
# --- Masterdata-Service-Integration (optional) ---
# ==============================================================
# - id: masterdata-service-route
# uri: lb://masterdata-service
# predicates:
# - Path=/api/masterdata/**
# filters:
# - StripPrefix=1
# - name: CircuitBreaker
# args:
# name: masterdataCircuitBreaker
# fallbackUri: forward:/fallback/masterdata
# ==============================================================
# --- Auth-Service-Integration (optional) ---
# ==============================================================
# - id: auth-service-route
# uri: lb://auth-service
# predicates:
# - Path=/api/auth/**
# filters:
# - StripPrefix=1
# - name: CircuitBreaker
# args:
# name: authCircuitBreaker
# fallbackUri: forward:/fallback/auth
# Circuit Breaker Konfiguration
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 100
minimumNumberOfCalls: 20
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 50
eventConsumerBufferSize: 10
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.util.concurrent.TimeoutException
- java.io.IOException
instances:
defaultCircuitBreaker:
baseConfig: default
pingCircuitBreaker:
baseConfig: default
membersCircuitBreaker:
baseConfig: default
slidingWindowSize: 50
horsesCircuitBreaker:
baseConfig: default
slidingWindowSize: 50
eventsCircuitBreaker:
baseConfig: default
slidingWindowSize: 75
masterdataCircuitBreaker:
baseConfig: default
slidingWindowSize: 30
authCircuitBreaker:
baseConfig: default
slidingWindowSize: 20
failureRateThreshold: 30
# Management und Monitoring
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,gateway,circuitbreakers
base-path: /actuator
cors:
allowed-origins:
- "https://*.meldestelle.at"
- "http://localhost:*"
allowed-methods: GET,POST
allowed-headers: "*"
allow-credentials: true
endpoint:
health:
show-details: when_authorized
show-components: when_authorized
probes:
enabled: true
metrics:
access: unrestricted
info:
access: unrestricted
prometheus:
access: unrestricted
gateway:
access: unrestricted
circuitbreakers:
enabled: true
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5,0.90,0.95,0.99
minimum-expected-value:
http.server.requests: 1ms
maximum-expected-value:
http.server.requests: 30s
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
instance: ${spring.cloud.consul.discovery.instance-id}
service: gateway
component: infrastructure
gateway: api-gateway
info:
env:
enabled: true
git:
mode: full
build:
enabled: true
java:
enabled: true
# Tracing-Konfiguration - Aktiviert (Micrometer Tracing + Zipkin)
tracing:
enabled: ${TRACING_ENABLED:false}
sampling:
probability: ${TRACING_SAMPLING_PROBABILITY:1.0}
zipkin:
tracing:
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
# Reduziert Verbindungsfehler, wenn Zipkin nicht verfügbar ist
connect-timeout: 1s
read-timeout: 10s
# Erweiterte Logging-Konfiguration
logging:
level:
org.springframework.cloud.gateway: INFO
org.springframework.cloud.loadbalancer: INFO
org.springframework.cloud.consul: INFO
at.mocode.infrastructure.gateway: INFO
io.github.resilience4j: INFO
reactor.netty.http.client: INFO
# (Redis-Konfiguration wurde in den bestehenden spring.data.redis-Block oben integriert)
@@ -3,7 +3,7 @@ package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.config.TestSecurityConfig
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.springframework.context.annotation.Import
import org.springframework.http.HttpStatus
import org.springframework.test.context.ActiveProfiles
@@ -13,31 +13,7 @@ import org.springframework.test.web.reactive.server.WebTestClient
* Tests für den Fallback Controller, der Circuit Breaker Szenarien behandelt.
* Testet alle Fallback-Endpunkte für verschiedene Services.
*/
@SpringBootTest(
classes = [GatewayApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
// Externe Abhängigkeiten für Fallback-Tests deaktivieren
"spring.cloud.discovery.enabled=false",
"spring.cloud.consul.enabled=false",
"spring.cloud.consul.config.enabled=false",
"spring.cloud.consul.discovery.register=false",
"spring.cloud.loadbalancer.enabled=false",
// Circuit Breaker Health Indicator deaktivieren um Interferenzen zu vermeiden
"resilience4j.circuitbreaker.configs.default.registerHealthIndicator=false",
"management.health.circuitbreakers.enabled=false",
// Custom Filter für reine Fallback-Tests deaktivieren
"gateway.security.jwt.enabled=false",
// Reaktiven Web-Anwendungstyp verwenden
"spring.main.web-application-type=reactive",
// Gateway Discovery deaktivieren
"spring.cloud.gateway.server.webflux.discovery.locator.enabled=false",
// Actuator Security deaktivieren
"management.security.enabled=false",
// Zufälligen Port setzen
"server.port=0"
]
)
@GatewayTestContext
@ActiveProfiles("test")
@Import(TestSecurityConfig::class)
class FallbackControllerTests {
@@ -0,0 +1,33 @@
package at.mocode.infrastructure.gateway
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration
import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
import org.springframework.context.annotation.ComponentScan
/**
* Test-spezifische, minimale GatewayApplication. Diese Klasse überschattet die Produktions-
* `GatewayApplication` während der Tests und deaktiviert problematische Auto-Konfigurationen,
* lädt aber weiterhin unsere Komponenten aus dem Gateway-Paket.
*/
@SpringBootConfiguration
@ComponentScan(basePackages = ["at.mocode.infrastructure.gateway"])
@ImportAutoConfiguration(
exclude = [
// Spring Cloud Refresh/Context (CNF in Tests vermeiden)
RefreshAutoConfiguration::class,
// HTTP/WebClient in Basis-Context-Load-Tests nicht erforderlich
HttpClientAutoConfiguration::class,
WebClientAutoConfiguration::class,
// Security AutoConfigs minimieren
ReactiveOAuth2ResourceServerAutoConfiguration::class,
SecurityAutoConfiguration::class,
ReactiveSecurityAutoConfiguration::class
]
)
class GatewayApplication
@@ -2,6 +2,10 @@ package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.config.TestSecurityConfig
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles
@@ -11,8 +15,8 @@ import org.springframework.test.context.ActiveProfiles
* Verwendet ein Test-Profil, um Produktions-Filter und externe Abhängigkeiten zu deaktivieren.
*/
@SpringBootTest(
classes = [GatewayApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = [MinimalTestApp::class],
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = [
// Alle externen Abhängigkeiten für Context-Loading-Test deaktivieren
"spring.cloud.discovery.enabled=false",
@@ -25,8 +29,8 @@ import org.springframework.test.context.ActiveProfiles
"management.health.circuitbreakers.enabled=false",
// Custom Security und Filter deaktivieren
"gateway.security.jwt.enabled=false",
// Reaktiven Web-Anwendungstyp verwenden
"spring.main.web-application-type=reactive",
// Für diesen Kontext-Load-Test keinen Web-Stack initialisieren
"spring.main.web-application-type=none",
// Gateway Discovery deaktivieren (korrekte Property)
"spring.cloud.gateway.discovery.locator.enabled=false",
// Zufälligen Port setzen
@@ -34,7 +38,8 @@ import org.springframework.test.context.ActiveProfiles
]
)
@ActiveProfiles("test")
@Import(TestSecurityConfig::class)
@EnableAutoConfiguration
@Import(TestSecurityConfig::class, TestSupportConfig::class)
class GatewayApplicationTests {
@Test
@@ -4,7 +4,7 @@ import at.mocode.infrastructure.gateway.config.TestSecurityConfig
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.context.annotation.Bean
@@ -20,32 +20,8 @@ import org.springframework.web.bind.annotation.RestController
* Tests for Gateway custom filters: CorrelationId, Enhanced Logging, and Rate Limiting.
* Tests filter behavior without disabling them (unlike other test classes).
*/
@SpringBootTest(
classes = [GatewayApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
// Disable external dependencies
"spring.cloud.discovery.enabled=false",
"spring.cloud.consul.enabled=false",
"spring.cloud.consul.config.enabled=false",
"spring.cloud.consul.discovery.register=false",
"spring.cloud.loadbalancer.enabled=false",
// Disable circuit breaker for filter tests
"resilience4j.circuitbreaker.configs.default.registerHealthIndicator=false",
"management.health.circuitbreakers.enabled=false",
// Keep custom filters enabled for testing
"gateway.security.jwt.enabled=false", // Disable JWT but keep other filters
// Use reactive web application type
"spring.main.web-application-type=reactive",
// Disable gateway discovery - use explicit routes
"spring.cloud.gateway.server.webflux.discovery.locator.enabled=false",
// Disable actuator security
"management.security.enabled=false",
// Set random port
"server.port=0"
]
)
@ActiveProfiles("dev") // Use dev profile to enable filters
@GatewayTestContext
@ActiveProfiles("test")
@AutoConfigureWebTestClient
@Import(TestSecurityConfig::class, GatewayFiltersTests.TestFilterConfig::class)
class GatewayFiltersTests {
@@ -1,16 +1,15 @@
package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.config.TestSecurityConfig
import at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
@@ -21,32 +20,7 @@ import org.springframework.web.bind.annotation.RestController
* Tests for Gateway routing functionality.
* Uses mock backend services to test route forwarding.
*/
@SpringBootTest(
classes = [GatewayApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
// Disable external dependencies
"spring.cloud.discovery.enabled=false",
"spring.cloud.consul.enabled=false",
"spring.cloud.consul.config.enabled=false",
"spring.cloud.consul.discovery.register=false",
"spring.cloud.loadbalancer.enabled=false",
// Disable circuit breaker for routing tests
"resilience4j.circuitbreaker.configs.default.registerHealthIndicator=false",
"management.health.circuitbreakers.enabled=false",
// Disable custom filters for pure routing tests
"gateway.security.jwt.enabled=false",
// Use reactive web application type
"spring.main.web-application-type=reactive",
// Disable gateway discovery - use explicit routes
"spring.cloud.gateway.server.webflux.discovery.locator.enabled=false",
// Disable actuator security
"management.security.enabled=false",
// Set random port
"server.port=0"
]
)
@ActiveProfiles("test")
@GatewayTestContext
@AutoConfigureWebTestClient
@Import(TestSecurityConfig::class, GatewayRoutingTests.TestRoutesConfig::class)
class GatewayRoutingTests {
@@ -5,7 +5,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
@@ -20,32 +20,8 @@ import org.springframework.web.bind.annotation.*
* Tests for Gateway security configuration including CORS settings.
* Tests the overall security setup and cross-origin request handling.
*/
@SpringBootTest(
classes = [GatewayApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
// Disable external dependencies
"spring.cloud.discovery.enabled=false",
"spring.cloud.consul.enabled=false",
"spring.cloud.consul.config.enabled=false",
"spring.cloud.consul.discovery.register=false",
"spring.cloud.loadbalancer.enabled=false",
// Disable circuit breaker for security tests
"resilience4j.circuitbreaker.configs.default.registerHealthIndicator=false",
"management.health.circuitbreakers.enabled=false",
// Disable JWT for CORS testing
"gateway.security.jwt.enabled=false",
// Use reactive web application type
"spring.main.web-application-type=reactive",
// Disable gateway discovery - use explicit routes
"spring.cloud.gateway.server.webflux.discovery.locator.enabled=false",
// Disable actuator security
"management.security.enabled=false",
// Set random port
"server.port=0"
]
)
@ActiveProfiles("test") // Use test profile to disable unrelated global filters; CORS config is present in application-test.yml
@GatewayTestContext
@ActiveProfiles("test") // Behalte test-Profil explizit für Klarheit
@AutoConfigureWebTestClient
@Import(TestSecurityConfig::class, GatewaySecurityTests.TestSecurityConfig::class)
class GatewaySecurityTests {
@@ -2,7 +2,7 @@ package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.config.TestSecurityConfig
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestPropertySource
@@ -13,7 +13,7 @@ import org.springframework.test.context.TestPropertySource
* without requiring actual Testcontainers, focusing on resolving the OAuth2 ResourceServer
* autoconfiguration timing issue.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@GatewayTestContext
@ActiveProfiles("keycloak-integration-test")
@TestPropertySource(
properties = [
@@ -0,0 +1,10 @@
package at.mocode.infrastructure.gateway
import org.springframework.boot.autoconfigure.SpringBootApplication
/**
* Minimaler Test-ApplicationContext, der nur die absolut nötigen Auto-Konfigurationen lädt.
* Problematische Auto-Configs werden hier explizit ausgeschlossen, damit der Context sicher startet.
*/
@SpringBootApplication
class MinimalTestApp
@@ -0,0 +1,11 @@
package at.mocode.infrastructure.gateway
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.web.reactive.function.client.WebClient
@TestConfiguration
class TestSupportConfig {
@Bean
fun webClientBuilder(): WebClient.Builder = WebClient.builder()
}
@@ -0,0 +1,43 @@
package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@GatewayTestContext
@Import(WebFluxSmokeTest.SmokeConfig::class)
class WebFluxSmokeTest {
@Autowired
lateinit var webTestClient: WebTestClient
@Test
fun `should load reactive web context and serve smoke endpoint`() {
webTestClient.get()
.uri("/smoke")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("ok")
}
@Configuration
class SmokeConfig {
@Bean
fun smokeController(): SmokeController = SmokeController()
}
@RestController
@RequestMapping
class SmokeController {
@GetMapping("/smoke")
fun smoke(): String = "ok"
}
}
@@ -0,0 +1,53 @@
package at.mocode.infrastructure.gateway.support
import at.mocode.infrastructure.gateway.MinimalTestApp
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Profile
import org.springframework.test.context.ActiveProfiles
import org.springframework.context.annotation.Import
/**
* Zentrale Meta-Annotation für Gateway-Tests.
*
* - Lädt einen minimalen Spring-Boot-Kontext über `MinimalTestApp`.
* - Erzwingt das `test`-Profil.
* - Schließt laute/unnötige Auto-Konfigurationen für schnelle, stabile Context-Loads aus.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@SpringBootTest(
classes = [MinimalTestApp::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
// Cloud/Discovery im Test deaktivieren
"spring.cloud.discovery.enabled=false",
"spring.cloud.consul.enabled=false",
"spring.cloud.consul.config.enabled=false",
"spring.cloud.consul.discovery.register=false",
"spring.cloud.loadbalancer.enabled=false",
// Circuit Breaker Health aus
"resilience4j.circuitbreaker.configs.default.registerHealthIndicator=false",
"management.health.circuitbreakers.enabled=false",
// Gateway Discovery Locator aus
"spring.cloud.gateway.discovery.locator.enabled=false",
// Reaktiven WebStack initialisieren (für WebTestClient)
"spring.main.web-application-type=reactive",
// Zufälliger Port verhindert Port-Konflikte
"server.port=0"
]
)
@ActiveProfiles("test")
@ImportAutoConfiguration(
exclude = [
// Nur die wirklich lauten/unnötigen AutoConfigs im DefaultTestprofil deaktivieren
// Spring Cloud Refresh (verursachte CNF in früheren Läufen)
org.springframework.cloud.autoconfigure.RefreshAutoConfiguration::class,
// Security Resource Server (Keycloak) für die meisten Tests nicht nötig
ReactiveOAuth2ResourceServerAutoConfiguration::class
]
)
@Profile("test")
annotation class GatewayTestContext
@@ -0,0 +1,8 @@
package at.mocode.infrastructure.gateway.support
/**
* Platzhalter-Klasse: Die frühere ContextCustomizerFactory wurde entfernt,
* um Kompilationsfehler zu vermeiden. Die Test-Excludes werden nun über
* junit-platform.properties und application-test.yaml gesetzt.
*/
class TestAutoConfigExcluderPlaceholder
@@ -0,0 +1,3 @@
// DEPRECATED: Diese Datei wurde absichtlich geleert, um @EnableWebFlux im Testkontext zu vermeiden,
// da sie die WebFluxAutoConfiguration deaktiviert. Bitte nicht wieder aktivieren.
package at.mocode.infrastructure.gateway.support
@@ -0,0 +1 @@
# Deaktiviert: zentrale ContextCustomizerFactory wurde entfernt
@@ -1,3 +1,4 @@
# migrated from application-dev.yml (standardized to .yaml)
server:
port: 0
@@ -26,8 +27,7 @@ spring:
discovery:
locator:
enabled: false
routes:
[ ]
routes: [ ]
globalcors:
cors-configurations:
'[/**]':
@@ -45,7 +45,6 @@ spring:
- "*"
allowCredentials: true
maxAge: 3600
# Override production routes: keep empty in tests running with dev profile
management:
endpoints:
@@ -1,15 +1,14 @@
# migrated from application-keycloak-integration-test.yml (standardized to .yaml)
server:
port: 0
spring:
application:
name: api-gateway-test
name: api-gateway-keycloak-integration-test
main:
web-application-type: reactive
autoconfigure:
exclude:
# Disable OAuth2 ResourceServer autoconfiguration in tests
# use mock JwtAuthenticationFilter instead of real JWT validation
- org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
cloud:
discovery:
@@ -23,7 +22,6 @@ spring:
loadbalancer:
enabled: false
gateway:
# IMPORTANT: Do not load production lb:// routes in tests
server:
webflux:
discovery:
@@ -32,8 +30,7 @@ spring:
httpclient:
connect-timeout: 1000
response-timeout: 5s
routes:
[ ]
routes: [ ]
globalcors:
cors-configurations:
'[/**]':
@@ -63,8 +60,18 @@ management:
health:
circuit breakers:
enabled: false
security:
enabled: false
gateway:
security:
jwt:
enabled: false
keycloak:
enabled: true
logging:
level:
org.springframework.cloud.gateway: WARN
org.springframework.security: DEBUG
at.mocode.infrastructure.gateway: DEBUG
@@ -1,83 +0,0 @@
server:
port: 0
spring:
application:
name: api-gateway-keycloak-integration-test
main:
web-application-type: reactive
# Exclude OAuth2 ResourceServer auto-configuration to prevent early issuer-uri validation
# The OAuth2 configuration will be set dynamically after Testcontainers start
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
# OAuth2 configuration will be set by @DynamicPropertySource after containers start
# Do not set static issuer-uri here as it will fail validation before containers are ready
cloud:
discovery:
enabled: false
consul:
enabled: false
config:
enabled: false
discovery:
register: false
loadbalancer:
enabled: false
gateway:
# IMPORTANT: Do not load production lb:// routes in tests
server:
webflux:
discovery:
locator:
enabled: false
httpclient:
connect-timeout: 1000
response-timeout: 5s
routes:
[ ]
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns:
- "http://localhost:*"
- "https://*.meldestelle.at"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- PATCH
- OPTIONS
allowedHeaders:
- "*"
allowCredentials: true
maxAge: 3600
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
health:
circuit breakers:
enabled: false
security:
enabled: false
# Enable JWT authentication through OAuth2 Resource Server for integration testing
gateway:
security:
jwt:
enabled: false # Disable custom JWT filter
keycloak:
enabled: true # Enable Keycloak integration
logging:
level:
org.springframework.cloud.gateway: WARN
org.springframework.security: DEBUG
at.mocode.infrastructure.gateway: DEBUG
@@ -0,0 +1,28 @@
spring:
autoconfigure:
exclude: [ ]
main:
web-application-type: reactive
cloud:
refresh:
enabled: false
config:
enabled: false
bootstrap:
enabled: false
spring.cloud:
gateway:
enabled: true
# Keine weiteren Gateway-spezifischen AutoConfigs ausschließen, da nicht zwingend vorhanden
management:
health:
circuitbreakers:
enabled: false
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: false
@@ -0,0 +1,21 @@
spring.profiles.active=test
spring.main.allow-bean-definition-overriding=true
logging.level.org.springframework.boot.test=INFO
spring.test.context.failure.threshold=0
# Zentrale AutoConfiguration-Excludes (testweit). Bitte minimal halten und mit application-test.yaml abgleichen.
spring.autoconfigure.exclude=\
org.springframework.cloud.autoconfigure.RefreshAutoConfiguration,\
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration
# Spring Cloud im Test vollständig ruhigstellen
spring.cloud.refresh.enabled=false
spring.cloud.config.enabled=false
spring.cloud.bootstrap.enabled=false