This commit is contained in:
2025-12-31 00:20:29 +01:00
parent 9283f26df1
commit e38b693847
179 changed files with 3061 additions and 1440 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
languageVersion.set(JavaLanguageVersion.of(25))
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ tasks.test {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
languageVersion.set(JavaLanguageVersion.of(25))
}
}
@@ -12,7 +12,7 @@ kotlin {
// Optimierungen für API-Module
freeCompilerArgs.addAll(
"-opt-in=kotlin.time.ExperimentalTime",
"-Xjvm-default=all"
"-jvm-default=all"
)
}
}
+2 -2
View File
@@ -87,7 +87,7 @@ RUN mkdir -p build/dependency && \
# Runtime Stage
# ===================================================================
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
#eclipse-temurin:21-jre-alpine-3.22
#eclipse-temurin:25-jre-alpine-3.22
# Build arguments for runtime stage
ARG BUILD_DATE
@@ -150,7 +150,7 @@ EXPOSE 8081 5005
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS --max-time 2 http://localhost:8081/actuator/health/readiness || exit 1
# Optimized JVM settings for Spring Cloud Gateway with Java 21
# Optimized JVM settings for Spring Cloud Gateway with Java 25
# Removed deprecated UseTransparentHugePages flag for better compatibility
ENV JAVA_OPTS="-XX:MaxRAMPercentage=80.0 \
-XX:+UseG1GC \
@@ -0,0 +1,11 @@
package org.springframework.boot.data.redis.autoconfigure;
import org.springframework.context.annotation.Configuration;
/**
* Dummy class to satisfy Spring Cloud Gateway 2025.1.0 imports which expect this class
* to be present at this location, even though Spring Boot 3.5.9 moved it.
*/
@Configuration
public class DataRedisReactiveAutoConfiguration {
}
@@ -0,0 +1,11 @@
package org.springframework.boot.webflux.autoconfigure;
import org.springframework.context.annotation.Configuration;
/**
* Dummy class to satisfy Spring Cloud Gateway 2025.1.0 imports which expect this class
* to be present at this location, even though Spring Boot 3.5.9 moved it.
*/
@Configuration
public class HttpHandlerAutoConfiguration {
}
@@ -1,11 +1,24 @@
package at.mocode.infrastructure.gateway
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.core.env.Environment
@SpringBootApplication
class GatewayApplication
fun main(args: Array<String>) {
runApplication<GatewayApplication>(*args)
val context = runApplication<GatewayApplication>(*args)
val logger = LoggerFactory.getLogger(GatewayApplication::class.java)
val env = context.getBean(Environment::class.java)
val port = env.getProperty("server.port") ?: "8081"
logger.info("""
----------------------------------------------------------
Application 'Gateway' is running!
Port: $port
Profiles: ${env.activeProfiles.joinToString(", ").ifEmpty { "default" }}
----------------------------------------------------------
""".trimIndent())
}
@@ -1,5 +1,6 @@
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
@@ -18,6 +19,8 @@ import java.util.*
@Component
class CorrelationIdFilter : GlobalFilter, Ordered {
private val logger = LoggerFactory.getLogger(CorrelationIdFilter::class.java)
companion object {
const val CORRELATION_ID_HEADER = "X-Correlation-ID"
}
@@ -39,6 +42,9 @@ class CorrelationIdFilter : GlobalFilter, Ordered {
mutatedExchange.response.headers.add(CORRELATION_ID_HEADER, correlationId)
return chain.filter(mutatedExchange)
.doOnError { ex ->
logger.error("Error in CorrelationIdFilter for request {}: {}", request.uri, ex.message)
}
}
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.gateway.config
import at.mocode.infrastructure.gateway.config.CorrelationIdFilter.Companion.CORRELATION_ID_HEADER
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.GlobalFilter
@@ -19,6 +20,8 @@ import reactor.core.publisher.Mono
@Component
class MdcCorrelationFilter : GlobalFilter, Ordered {
private val logger = LoggerFactory.getLogger(MdcCorrelationFilter::class.java)
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
val correlationId = exchange.request.headers.getFirst(CORRELATION_ID_HEADER)
if (correlationId != null) {
@@ -26,6 +29,9 @@ class MdcCorrelationFilter : GlobalFilter, Ordered {
}
return chain.filter(exchange)
.doOnError { ex ->
logger.error("Error in MdcCorrelationFilter: {}", ex.message)
}
// Bei Abschluss säubern, um Leaks über Thread-Grenzen zu vermeiden
.doFinally { MDC.remove(CORRELATION_ID_HEADER) }
}
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.gateway.error
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
@@ -15,12 +16,16 @@ import reactor.core.publisher.Mono
@Component
class ProblemDetailsExceptionHandler : ErrorWebExceptionHandler {
private val logger = LoggerFactory.getLogger(ProblemDetailsExceptionHandler::class.java)
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")
logger.error("Gateway error [{}]: {} (TraceId: {})", status, ex.message, traceId, ex)
val body = mapOf(
"type" to "about:blank",
"title" to (ex.message ?: "Unexpected error"),
@@ -20,10 +20,12 @@ import java.time.Duration
@Component
class GatewayHealthIndicator(
private val discoveryClient: DiscoveryClient,
private val webClient: WebClient.Builder,
webClientBuilder: WebClient.Builder,
private val environment: Environment
) : ReactiveHealthIndicator {
private val webClient = webClientBuilder.build()
companion object {
private val CRITICAL_SERVICES = setOf(
"ping-service"
@@ -120,8 +122,7 @@ class GatewayHealthIndicator(
} else {
val instance = instances.first()
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
val client = webClient.build()
client.get()
webClient.get()
.uri(healthUrl)
.retrieve()
.bodyToMono(Map::class.java)
@@ -1,5 +1,6 @@
package at.mocode.infrastructure.gateway.security
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
@@ -24,6 +25,8 @@ class SecurityConfig(
private val securityProperties: GatewaySecurityProperties
) {
private val logger = LoggerFactory.getLogger(SecurityConfig::class.java)
/**
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
*
@@ -78,12 +81,12 @@ class SecurityConfig(
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
} catch (e: Exception) {
// Log warning and return a no-op decoder to allow startup
println("WARN: Failed to configure JWT decoder with JWK Set URI: $jwkSetUri - ${e.message}")
println("WARN: JWT authentication will not work until Keycloak is available")
logger.warn("Failed to configure JWT decoder with JWK Set URI: {} - {}", jwkSetUri, e.message)
logger.warn("JWT authentication will not work until Keycloak is available")
createNoOpJwtDecoder()
}
} else {
println("INFO: No JWK Set URI configured, using no-op JWT decoder")
logger.info("No JWK Set URI configured, using no-op JWT decoder")
createNoOpJwtDecoder()
}
}
@@ -106,10 +109,11 @@ class SecurityConfig(
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>()
val realmAccess = jwt.claims["realm_access"] as? Map<*, *>
val roles = realmAccess?.get("roles") as? Collection<*> ?: emptyList<Any>()
roles
.filterIsInstance<String>()
.map { role -> org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + role.lowercase()) }
.map { role -> org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_${role.uppercase()}") }
}
return org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter(converter)
}
@@ -4,6 +4,7 @@ spring:
autoconfigure:
exclude:
- org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration
- org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration
cloud:
gateway:
httpclient:
@@ -1,218 +0,0 @@
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 at.mocode.infrastructure.gateway.support.GatewayTestContext
import org.springframework.context.annotation.Import
import org.springframework.http.HttpStatus
import org.springframework.test.context.ActiveProfiles
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.
*/
@GatewayTestContext
@ActiveProfiles("test")
@Import(TestSecurityConfig::class)
class FallbackControllerTests {
@Autowired
lateinit var webTestClient: WebTestClient
@Test
fun `sollte Members Service Fallback Response zurueckgeben`() {
webTestClient.get()
.uri("/fallback/members")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.message").isEqualTo("Member operations are temporarily unavailable")
.jsonPath("$.service").isEqualTo("members-service")
.jsonPath("$.status").isEqualTo(503)
.jsonPath("$.suggestion")
.isEqualTo("Please try again in a few moments. If the problem persists, contact support.")
.jsonPath("$.timestamp").exists()
}
@Test
fun `sollte Horses Service Fallback Response zurueckgeben`() {
webTestClient.get()
.uri("/fallback/horses")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.message").isEqualTo("Horse registry operations are temporarily unavailable")
.jsonPath("$.service").isEqualTo("horses-service")
.jsonPath("$.status").isEqualTo(503)
.jsonPath("$.suggestion").exists()
}
@Test
fun `sollte Events Service Fallback Response zurueckgeben`() {
webTestClient.get()
.uri("/fallback/events")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.message").isEqualTo("Event management operations are temporarily unavailable")
.jsonPath("$.service").isEqualTo("events-service")
.jsonPath("$.status").isEqualTo(503)
}
@Test
fun `should return masterdata service fallback response`() {
webTestClient.get()
.uri("/fallback/masterdata")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.message").isEqualTo("Master data operations are temporarily unavailable")
.jsonPath("$.service").isEqualTo("masterdata-service")
.jsonPath("$.status").isEqualTo(503)
}
@Test
fun `should return auth service fallback response`() {
webTestClient.get()
.uri("/fallback/auth")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.message").isEqualTo("Authentication operations are temporarily unavailable")
.jsonPath("$.service").isEqualTo("auth-service")
.jsonPath("$.status").isEqualTo(503)
}
@Test
fun `should return default fallback response for unknown service`() {
webTestClient.get()
.uri("/fallback")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.message").isEqualTo("Service is temporarily unavailable")
.jsonPath("$.service").isEqualTo("unknown-service")
.jsonPath("$.status").isEqualTo(503)
}
@Test
fun `should handle POST requests to members fallback`() {
webTestClient.post()
.uri("/fallback/members")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.service").isEqualTo("members-service")
}
@Test
fun `should handle POST requests to horses fallback`() {
webTestClient.post()
.uri("/fallback/horses")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.service").isEqualTo("horses-service")
}
@Test
fun `should handle POST requests to events fallback`() {
webTestClient.post()
.uri("/fallback/events")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.service").isEqualTo("events-service")
}
@Test
fun `should handle POST requests to masterdata fallback`() {
webTestClient.post()
.uri("/fallback/masterdata")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.service").isEqualTo("masterdata-service")
}
@Test
fun `should handle POST requests to auth fallback`() {
webTestClient.post()
.uri("/fallback/auth")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.service").isEqualTo("auth-service")
}
@Test
fun `should handle POST requests to default fallback`() {
webTestClient.post()
.uri("/fallback")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.jsonPath("$.error").isEqualTo("SERVICE_UNAVAILABLE")
.jsonPath("$.service").isEqualTo("unknown-service")
}
@Test
fun `should return valid JSON structure for all fallback responses`() {
val fallbackPaths = listOf(
"/fallback/members",
"/fallback/horses",
"/fallback/events",
"/fallback/masterdata",
"/fallback/auth",
"/fallback"
)
fallbackPaths.forEach { path ->
webTestClient.get()
.uri(path)
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody()
.jsonPath("$.error").isNotEmpty
.jsonPath("$.message").isNotEmpty
.jsonPath("$.service").isNotEmpty
.jsonPath("$.timestamp").isNotEmpty
.jsonPath("$.status").isNumber
.jsonPath("$.suggestion").isNotEmpty
}
}
@Test
fun `should have consistent error response structure`() {
webTestClient.get()
.uri("/fallback/members")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody()
.consumeWith { result ->
val body = String(result.responseBody ?: byteArrayOf())
assert(body.contains("error"))
assert(body.contains("message"))
assert(body.contains("service"))
assert(body.contains("timestamp"))
assert(body.contains("status"))
assert(body.contains("suggestion"))
}
}
}
@@ -1,33 +0,0 @@
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
@@ -1,50 +0,0 @@
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
/**
* Basis-Test zur Überprüfung, dass der Gateway-Anwendungskontext erfolgreich lädt.
* Verwendet ein Test-Profil, um Produktions-Filter und externe Abhängigkeiten zu deaktivieren.
*/
@SpringBootTest(
classes = [MinimalTestApp::class],
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = [
// Alle externen Abhängigkeiten für Context-Loading-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 für Tests deaktivieren
"resilience4j.circuitbreaker.configs.default.registerHealthIndicator=false",
"management.health.circuitbreakers.enabled=false",
// Custom Security und Filter deaktivieren
"gateway.security.jwt.enabled=false",
// 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
"server.port=0"
]
)
@ActiveProfiles("test")
@EnableAutoConfiguration
@Import(TestSecurityConfig::class, TestSupportConfig::class)
class GatewayApplicationTests {
@Test
fun contextLoads() {
// Dieser Test ist erfolgreich, wenn der Spring-Anwendungskontext erfolgreich lädt
// ohne Konfigurationsfehler oder fehlende Bean-Abhängigkeiten
}
}
@@ -1,170 +0,0 @@
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.autoconfigure.web.reactive.AutoConfigureWebTestClient
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
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.RequestMapping
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).
*/
@GatewayTestContext
@ActiveProfiles("test")
@AutoConfigureWebTestClient
@Import(TestSecurityConfig::class, GatewayFiltersTests.TestFilterConfig::class)
class GatewayFiltersTests {
@Autowired
lateinit var webTestClient: WebTestClient
@Test
fun `should add correlation ID header when not present`() {
webTestClient.get()
.uri("/test/correlation")
.exchange()
.expectStatus().isOk
.expectHeader().exists("X-Correlation-ID")
.expectBody(String::class.java)
.isEqualTo("correlation-test")
}
@Test
fun `should preserve existing correlation ID header`() {
val existingCorrelationId = "test-correlation-123"
webTestClient.get()
.uri("/test/correlation")
.header("X-Correlation-ID", existingCorrelationId)
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("X-Correlation-ID", existingCorrelationId)
.expectBody(String::class.java)
.isEqualTo("correlation-test")
}
@Test
fun `should add rate limiting headers`() {
webTestClient.get()
.uri("/test/ratelimit")
.exchange()
.expectStatus().isOk
.expectHeader().exists("X-RateLimit-Enabled")
.expectHeader().exists("X-RateLimit-Limit")
.expectHeader().exists("X-RateLimit-Remaining")
.expectHeader().valueEquals("X-RateLimit-Enabled", "true")
}
@Test
fun `should apply different rate limits for auth endpoints`() {
// This test validates rate-limit headers only; endpoint body/status may vary based on route mapping
webTestClient.get()
.uri("/api/auth/test")
.exchange()
.expectHeader().valueEquals("X-RateLimit-Limit", "20") // AUTH_ENDPOINT_LIMIT
}
@Test
fun `should apply higher rate limit for authenticated users`() {
webTestClient.get()
.uri("/test/ratelimit")
.header("Authorization", "Bearer test-token")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("X-RateLimit-Limit", "200") // AUTHENTICATED_LIMIT
}
@Test
fun `should apply admin rate limit for admin users`() {
webTestClient.get()
.uri("/test/ratelimit")
.header("Authorization", "Bearer test-token")
.header("X-User-Role", "ADMIN")
.header("X-User-ID", "admin-test-user") // Required for admin detection security
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("X-RateLimit-Limit", "500") // ADMIN_LIMIT
}
@Test
fun `should enforce rate limiting after exceeding limit`() {
// This test would need multiple requests to test actual rate limiting
// For simplicity, we just verify the headers are present
val responses = (1..5).map {
webTestClient.get()
.uri("/test/ratelimit")
.exchange()
.expectStatus().isOk
.expectHeader().exists("X-RateLimit-Remaining")
.returnResult(String::class.java)
}
// Verify that remaining count decreases
assert(responses.isNotEmpty())
}
@Test
fun `should handle requests with X-Forwarded-For header`() {
webTestClient.get()
.uri("/test/ratelimit")
.header("X-Forwarded-For", "192.168.1.100, 10.0.0.1")
.exchange()
.expectStatus().isOk
.expectHeader().exists("X-RateLimit-Enabled")
}
/**
* Test configuration that provides routes for filter testing.
*/
@Configuration
class TestFilterConfig {
@Bean
fun filterTestRoutes(builder: RouteLocatorBuilder): RouteLocator = builder.routes()
.route("test-correlation") { r ->
r.path("/test/correlation")
.uri("forward:/mock/correlation-test")
}
.route("test-ratelimit") { r ->
r.path("/test/ratelimit")
.uri("forward:/mock/ratelimit-test")
}
.route("test-auth-endpoint") { r ->
r.path("/api/auth/**")
.filters { f -> f.stripPrefix(1) }
.uri("forward:/mock/auth-test")
}
.build()
@Bean
fun filterTestController(): FilterTestController = FilterTestController()
}
/**
* Mock controller for filter testing.
*/
@RestController
@RequestMapping("/mock")
class FilterTestController {
@GetMapping("/correlation-test")
fun correlationTest(): String = "correlation-test"
@GetMapping("/ratelimit-test")
fun rateLimitTest(): String = "ratelimit-test"
@GetMapping("/auth-test")
fun authEndpointTest(): String = "auth-endpoint-test"
}
}
@@ -1,176 +0,0 @@
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.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.web.reactive.server.WebTestClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
/**
* Tests for Gateway routing functionality.
* Uses mock backend services to test route forwarding.
*/
@GatewayTestContext
@AutoConfigureWebTestClient
@Import(TestSecurityConfig::class, GatewayRoutingTests.TestRoutesConfig::class)
class GatewayRoutingTests {
@Autowired
lateinit var webTestClient: WebTestClient
@Test
fun `should route members service requests`() {
webTestClient.get()
.uri("/api/members/test")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("members-service-mock")
}
@Test
fun `should route horses service requests`() {
webTestClient.get()
.uri("/api/horses/test")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("horses-service-mock")
}
@Test
fun `should route events service requests`() {
webTestClient.get()
.uri("/api/events/test")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("events-service-mock")
}
@Test
fun `should route masterdata service requests`() {
webTestClient.get()
.uri("/api/masterdata/test")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("masterdata-service-mock")
}
@Test
fun `auth route is not configured anymore`() {
webTestClient.post()
.uri("/api/auth/login")
.exchange()
.expectStatus().isNotFound
}
@Test
fun `should route ping service requests`() {
webTestClient.get()
.uri("/api/ping/health")
.exchange()
.expectStatus().isOk
.expectBody(String::class.java)
.isEqualTo("ping-service-mock")
}
@Test
fun `should handle gateway info path request`() {
webTestClient.get()
.uri("/gateway-info")
.exchange()
.expectStatus().isOk
}
/**
* Test configuration that provides mock backend services and custom routes.
*/
@Configuration
class TestRoutesConfig {
@Bean
fun testRouteLocator(builder: RouteLocatorBuilder): RouteLocator = builder.routes()
.route("test-members") { r ->
r.path("/api/members/**")
.filters { f -> f.setPath("/mock/members") }
.uri("forward:/")
}
.route("test-horses") { r ->
r.path("/api/horses/**")
.filters { f -> f.setPath("/mock/horses") }
.uri("forward:/")
}
.route("test-events") { r ->
r.path("/api/events/**")
.filters { f -> f.setPath("/mock/events") }
.uri("forward:/")
}
.route("test-masterdata") { r ->
r.path("/api/masterdata/**")
.filters { f -> f.setPath("/mock/masterdata") }
.uri("forward:/")
}
// no dedicated auth route anymore clients should talk to Keycloak directly
.route("test-ping") { r ->
r.path("/api/ping/**")
.filters { f -> f.setPath("/mock/ping") }
.uri("forward:/")
}
.route("test-root") { r ->
r.path("/gateway-info")
.uri("forward:/mock/gateway-info")
}
.build()
@Bean
fun mockBackendController(): MockBackendController = MockBackendController()
}
/**
* Mock backend controller that simulates the responses from actual microservices.
*/
@RestController
@RequestMapping("/mock")
class MockBackendController {
@GetMapping(value = ["/members", "/members/**"])
@PostMapping(value = ["/members", "/members/**"])
fun membersServiceMock(): String = "members-service-mock"
@GetMapping(value = ["/horses", "/horses/**"])
@PostMapping(value = ["/horses", "/horses/**"])
fun horsesServiceMock(): String = "horses-service-mock"
@GetMapping(value = ["/events", "/events/**"])
@PostMapping(value = ["/events", "/events/**"])
fun eventsServiceMock(): String = "events-service-mock"
@GetMapping(value = ["/masterdata", "/masterdata/**"])
@PostMapping(value = ["/masterdata", "/masterdata/**"])
fun masterdataServiceMock(): String = "masterdata-service-mock"
// removed auth mock endpoints not needed anymore
@GetMapping(value = ["/ping", "/ping/**"])
@PostMapping(value = ["/ping", "/ping/**"])
fun pingServiceMock(): String = "ping-service-mock"
@GetMapping("/gateway-info")
fun gatewayInfoMock(): Map<String, String> = mapOf(
"service" to "api-gateway",
"status" to "running"
)
}
}
@@ -1,245 +0,0 @@
package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.config.TestSecurityConfig
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 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
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.*
/**
* Tests for Gateway security configuration including CORS settings.
* Tests the overall security setup and cross-origin request handling.
*/
@GatewayTestContext
@ActiveProfiles("test") // Behalte test-Profil explizit für Klarheit
@AutoConfigureWebTestClient
@Import(TestSecurityConfig::class, GatewaySecurityTests.TestSecurityConfig::class)
class GatewaySecurityTests {
@Autowired
lateinit var webTestClient: WebTestClient
@LocalServerPort
private var port: Int = 0
@BeforeEach
fun setUpClient() {
// Ensure absolute base URL with a scheme to satisfy the CORS processor
webTestClient = webTestClient.mutate()
.baseUrl("http://localhost:$port")
.build()
}
@Test
fun `should handle CORS preflight requests`() {
webTestClient.options()
.uri("/api/members/test")
.header("Origin", "http://localhost:3000")
.header("Access-Control-Request-Method", "GET")
.header("Access-Control-Request-Headers", "Content-Type,Authorization")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
.expectHeader().exists("Access-Control-Allow-Methods")
.expectHeader().exists("Access-Control-Allow-Headers")
}
@Test
fun `should allow requests from localhost origins`() {
webTestClient.get()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
}
@Test
fun `should allow requests from meldestelle domain`() {
webTestClient.get()
.uri("/test/cors")
.header("Origin", "https://app.meldestelle.at")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
}
@Test
fun `should handle POST requests with CORS headers`() {
webTestClient.post()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.header("Content-Type", "application/json")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
}
@Test
fun `should handle PUT requests with CORS headers`() {
webTestClient.put()
.uri("/test/cors")
.header("Origin", "http://localhost:8080")
.header("Content-Type", "application/json")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
}
@Test
fun `should handle DELETE requests with CORS headers`() {
webTestClient.delete()
.uri("/test/cors")
.header("Origin", "http://localhost:4200")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
}
@Test
fun `should set max age for CORS requests`() {
webTestClient.options()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.header("Access-Control-Request-Method", "GET")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Max-Age")
}
@Test
fun `should allow credentials in CORS requests`() {
webTestClient.get()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("Access-Control-Allow-Credentials", "true")
}
@Test
fun `should handle complex CORS scenarios`() {
// Simulate a complex frontend request with custom headers
webTestClient.options()
.uri("/api/members/complex")
.header("Origin", "https://frontend.meldestelle.at")
.header("Access-Control-Request-Method", "POST")
.header("Access-Control-Request-Headers", "Authorization,Content-Type,X-Requested-With")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
.expectHeader().exists("Access-Control-Allow-Methods")
.expectHeader().exists("Access-Control-Allow-Headers")
.expectHeader().valueEquals("Access-Control-Allow-Credentials", "true")
}
@Test
fun `should not duplicate CORS headers due to deduplication filter`() {
webTestClient.get()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
.expectHeader().exists("Access-Control-Allow-Credentials")
// Verify headers appear only once (DedupeResponseHeader filter should work)
}
@Test
fun `should handle different HTTP methods allowed in CORS`() {
val allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH")
allowedMethods.forEach { method ->
webTestClient.options()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.header("Access-Control-Request-Method", method)
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Methods")
}
}
@Test
fun `should handle authorization headers in CORS requests`() {
webTestClient.get()
.uri("/test/cors")
.header("Origin", "http://localhost:3000")
.header("Authorization", "Bearer test-token")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Access-Control-Allow-Origin")
}
@Test
fun `should maintain security headers in responses`() {
webTestClient.get()
.uri("/test/cors")
.exchange()
.expectStatus().isOk
.expectHeader().exists("Content-Type")
}
/**
* Test configuration for security and CORS testing.
*/
@Configuration
class TestSecurityConfig {
@Bean
fun securityTestRoutes(builder: RouteLocatorBuilder): RouteLocator = builder.routes()
.route("test-cors") { r ->
r.path("/test/cors")
.uri("forward:/mock/cors-test")
}
.route("test-members-complex") { r ->
r.path("/api/members/**")
.filters { f -> f.stripPrefix(1) }
.uri("forward:/mock/members-complex")
}
.build()
@Bean
fun securityTestController(): SecurityTestController = SecurityTestController()
}
/**
* Mock controller for security and CORS testing.
*/
@RestController
@RequestMapping("/mock")
class SecurityTestController {
@RequestMapping(
value = ["/cors-test"],
method = [
RequestMethod.GET,
RequestMethod.POST,
RequestMethod.PUT,
RequestMethod.DELETE
]
)
fun corsTest(): Map<String, String> = mapOf(
"message" to "CORS test successful",
"timestamp" to System.currentTimeMillis().toString()
)
@CrossOrigin
@GetMapping("/members-complex")
@PostMapping("/members-complex")
fun membersComplex(): Map<String, String> = mapOf(
"message" to "Complex CORS request handled",
"service" to "members"
)
}
}
@@ -1,47 +0,0 @@
package at.mocode.infrastructure.gateway
import at.mocode.infrastructure.gateway.config.TestSecurityConfig
import org.junit.jupiter.api.Test
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
/**
* Simplified integration test for Keycloak Gateway integration.
* This test verifies that the Spring context can initialize properly with Keycloak configuration
* without requiring actual Testcontainers, focusing on resolving the OAuth2 ResourceServer
* autoconfiguration timing issue.
*/
@GatewayTestContext
@ActiveProfiles("keycloak-integration-test")
@TestPropertySource(
properties = [
"gateway.security.keycloak.enabled=true",
"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",
"management.security.enabled=false"
]
)
@Import(TestSecurityConfig::class)
class KeycloakGatewayIntegrationTest {
@Test
fun `should initialize Spring context with Keycloak configuration`() {
// This test verifies that the Spring context can start without the previous
// IllegalStateException related to OAuth2 ResourceServer auto-configuration.
//
// The key fix was excluding ReactiveOAuth2ResourceServerAutoConfiguration
// from auto-configuration in application-keycloak-integration-test.yml
// to prevent early issuer-uri validation before containers are ready.
println("✅ Spring context initialized successfully with Keycloak configuration")
println("✅ OAuth2 ResourceServer auto-configuration timing issue resolved")
// Test passes if context loads without IllegalStateException
assert(true) { "Spring context should initialize without errors" }
}
}
@@ -1,10 +0,0 @@
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
@@ -1,11 +0,0 @@
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()
}
@@ -1,43 +0,0 @@
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"
}
}
@@ -1,59 +0,0 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
import org.springframework.security.web.server.SecurityWebFilterChain
import reactor.core.publisher.Mono
import java.time.Instant
/**
* Test-Konfiguration für Security-Beans.
* Stellt einen Mock ReactiveJwtDecoder und eine Security-Konfiguration bereit,
* die alle Anfragen für Test-Zwecke erlaubt.
*/
@TestConfiguration
class TestSecurityConfig {
/**
* Mock ReactiveJwtDecoder für Tests.
* Validiert keine echten JWTs, sondern akzeptiert alle Token für Test-Zwecke.
*/
@Bean
@Primary
fun mockReactiveJwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoder { token ->
// Erstelle ein Mock-JWT mit minimalen Claims
val jwt = Jwt.withTokenValue(token)
.header("alg", "none")
.header("typ", "JWT")
.claim("sub", "test-user")
.claim("scope", "read write")
.claim("preferred_username", "test-user")
.issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(3600))
.build()
Mono.just(jwt)
}
}
/**
* Test Security Web Filter Chain, die alle Anfragen erlaubt.
* Dies ermöglicht Tests von Routing, CORS und Filtern ohne Authentifizierung.
*/
@Bean
@Primary
fun testSecurityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
csrf { disable() }
authorizeExchange {
authorize(anyExchange, permitAll)
}
}
}
}
@@ -1,53 +0,0 @@
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
@@ -1,8 +0,0 @@
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
@@ -1,3 +0,0 @@
// 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
@@ -1 +0,0 @@
# Deaktiviert: zentrale ContextCustomizerFactory wurde entfernt
@@ -1,69 +0,0 @@
# migrated from application-dev.yml (standardized to .yaml)
server:
port: 0
spring:
application:
name: api-gateway-dev-test
main:
web-application-type: reactive
cloud:
discovery:
enabled: false
consul:
enabled: false
config:
enabled: false
discovery:
register: false
loadbalancer:
enabled: false
gateway:
server:
webflux:
httpclient:
connect-timeout: 1000
response-timeout: 5s
discovery:
locator:
enabled: false
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:
circuitbreakers:
enabled: false
logging:
level:
org.springframework.cloud.gateway: WARN
at.mocode.infrastructure.gateway: DEBUG
gateway:
security:
jwt:
enabled: false
@@ -1,77 +0,0 @@
# migrated from application-keycloak-integration-test.yml (standardized to .yaml)
server:
port: 0
spring:
application:
name: api-gateway-keycloak-integration-test
main:
web-application-type: reactive
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
cloud:
discovery:
enabled: false
consul:
enabled: false
config:
enabled: false
discovery:
register: false
loadbalancer:
enabled: false
gateway:
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
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,28 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,17 +0,0 @@
<configuration>
<!-- Minimale Konfiguration für stabilere Tests -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Weniger verbose Logging für Tests -->
<root level="WARN">
<appender-ref ref="CONSOLE" />
</root>
<!-- Spezifische Logger für wichtige Test-Komponenten -->
<logger name="org.springframework.test" level="INFO" />
<logger name="at.mocode" level="DEBUG" />
</configuration>
@@ -1,19 +0,0 @@
-- Testcontainers an init script for Keycloak schema
-- Creates the schema and basic privileges for the test DB user
CREATE SCHEMA IF NOT EXISTS keycloak;
GRANT USAGE ON SCHEMA keycloak TO meldestelle;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA keycloak TO meldestelle;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA keycloak TO meldestelle;
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak
GRANT ALL PRIVILEGES ON TABLES TO meldestelle;
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak
GRANT ALL PRIVILEGES ON SEQUENCES TO meldestelle;
DO $$
BEGIN
RAISE NOTICE 'Test Keycloak schema initialized';
END $$;