Introduce Ktor-based HTTP server for Masterdata context, implement upsert logic for Altersklasse, Bundesland, and Land repositories, enhance IdempotencyPlugin, and add integration tests.
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
package at.mocode.masterdata.service.api
|
||||
|
||||
import at.mocode.masterdata.service.MasterdataServiceApplication
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
|
||||
@SpringBootTest(
|
||||
classes = [MasterdataServiceApplication::class],
|
||||
properties = [
|
||||
"spring.main.web-application-type=none",
|
||||
"masterdata.http.port=18091" // fixed port for tests to simplify port discovery
|
||||
]
|
||||
)
|
||||
@ActiveProfiles("test")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class IdempotencyApiIntegrationTest {
|
||||
|
||||
private lateinit var baseUri: String
|
||||
|
||||
@BeforeAll
|
||||
fun setUp() {
|
||||
baseUri = "http://localhost:18091"
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDown() {
|
||||
// Server lifecycle managed by Spring; no explicit stop here.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `second POST with same Idempotency-Key returns identical response and does not create duplicate`() {
|
||||
val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build()
|
||||
|
||||
val payload = """
|
||||
{
|
||||
"isoAlpha2Code": "ZZ",
|
||||
"isoAlpha3Code": "ZZY",
|
||||
"nameDeutsch": "Testland Idempotency"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val key = "itest-key-001"
|
||||
|
||||
val req1 = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUri/countries"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Idempotency-Key", key)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||
.build()
|
||||
|
||||
val res1 = sendWithRetry(client, req1)
|
||||
|
||||
val req2 = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUri/countries"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Idempotency-Key", key)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||
.build()
|
||||
|
||||
val res2 = sendWithRetry(client, req2)
|
||||
|
||||
// Beide Antworten müssen identisch sein (Status + Body)
|
||||
assertThat(res1.statusCode()).isIn(200, 201) // Created or OK
|
||||
assertThat(res2.statusCode()).isEqualTo(res1.statusCode())
|
||||
assertThat(res2.headers().firstValue("Content-Type").orElse("application/json")).contains("application/json")
|
||||
assertThat(res2.body()).isEqualTo(res1.body())
|
||||
|
||||
// Verifizieren, dass kein Duplikat erzeugt wurde:
|
||||
// GET /countries?search=Testland%20Idempotency&unpaged=true sollte genau 1 Element enthalten
|
||||
val resList = sendWithRetry(
|
||||
client,
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUri/countries?search=Testland%20Idempotency&unpaged=true"))
|
||||
.GET()
|
||||
.build()
|
||||
)
|
||||
|
||||
assertThat(resList.statusCode()).isEqualTo(200)
|
||||
// sehr leichte Prüfung auf genau ein Ergebnis: zähle Vorkommen der isoAlpha3Code in JSON
|
||||
val occurrences = Regex("\"isoAlpha3Code\"\\s*:\\s*\"ZZY\"").findAll(resList.body()).count()
|
||||
assertThat(occurrences).isEqualTo(1)
|
||||
}
|
||||
|
||||
private fun sendWithRetry(client: HttpClient, request: HttpRequest, attempts: Int = 10, delayMillis: Long = 100): HttpResponse<String> {
|
||||
var lastEx: Exception? = null
|
||||
repeat(attempts) { idx ->
|
||||
try {
|
||||
return client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
} catch (e: Exception) {
|
||||
lastEx = e
|
||||
if (idx == attempts - 1) throw e
|
||||
Thread.sleep(delayMillis)
|
||||
}
|
||||
}
|
||||
throw lastEx ?: IllegalStateException("unreachable")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user