1+ package io.sdkman
2+
3+ import arrow.core.None
4+ import arrow.core.some
5+ import io.kotest.core.spec.style.ShouldSpec
6+ import io.kotest.matchers.shouldBe
7+ import io.ktor.client.request.header
8+ import io.ktor.client.request.post
9+ import io.ktor.client.request.setBody
10+ import io.ktor.http.ContentType
11+ import io.ktor.http.HttpHeaders
12+ import io.ktor.http.HttpStatusCode
13+ import io.ktor.http.contentType
14+ import io.sdkman.domain.Platform
15+ import io.sdkman.domain.Version
16+ import io.sdkman.support.selectVersion
17+ import io.sdkman.support.toJsonString
18+ import io.sdkman.support.withCleanDatabase
19+ import io.sdkman.support.withTestApplication
20+
21+ // testuser:password123 base64 encoded
22+ private const val BasicAuthHeader = " Basic dGVzdHVzZXI6cGFzc3dvcmQxMjM="
23+
24+ class IdempotentPostVersionApiSpec : ShouldSpec ({
25+
26+ should("POST be idempotent - same version posted twice should succeed with 201") {
27+ val version = Version (
28+ candidate = "java",
29+ version = "17.0.2",
30+ platform = Platform .LINUX_X64 ,
31+ url = "https://java-17.0.2-original",
32+ visible = true,
33+ vendor = "temurin".some(),
34+ md5sum = "original-hash".some()
35+ )
36+ val requestBody = version.toJsonString()
37+
38+ withCleanDatabase {
39+ withTestApplication {
40+ // First POST
41+ val response1 = client.post("/versions") {
42+ contentType(ContentType .Application .Json )
43+ setBody(requestBody)
44+ header(HttpHeaders .Authorization , BasicAuthHeader )
45+ }
46+ response1.status shouldBe HttpStatusCode .Companion .Created
47+
48+ // Second POST (idempotent)
49+ val response2 = client.post("/versions") {
50+ contentType(ContentType .Application .Json )
51+ setBody(requestBody)
52+ header(HttpHeaders .Authorization , BasicAuthHeader )
53+ }
54+ response2.status shouldBe HttpStatusCode .Companion .Created
55+ }
56+ // Verify version exists in database
57+ selectVersion(
58+ candidate = version.candidate,
59+ version = version.version,
60+ vendor = version.vendor,
61+ platform = version.platform
62+ ) shouldBe version.some()
63+ }
64+ }
65+
66+ should("POST overwrite existing version with different data") {
67+ val originalVersion = Version (
68+ candidate = "java",
69+ version = "17.0.3",
70+ platform = Platform .LINUX_X64 ,
71+ url = "https://java-17.0.3-original",
72+ visible = true,
73+ vendor = "temurin".some(),
74+ md5sum = "original-hash".some()
75+ )
76+
77+ val updatedVersion = Version (
78+ candidate = "java",
79+ version = "17.0.3",
80+ platform = Platform .LINUX_X64 ,
81+ url = "https://java-17.0.3-updated",
82+ visible = false,
83+ vendor = "temurin".some(),
84+ sha256sum = "updated-hash".some()
85+ )
86+
87+ withCleanDatabase {
88+ withTestApplication {
89+ // First POST
90+ val response1 = client.post("/versions") {
91+ contentType(ContentType .Application .Json )
92+ setBody(originalVersion.toJsonString())
93+ header(HttpHeaders .Authorization , BasicAuthHeader )
94+ }
95+ response1.status shouldBe HttpStatusCode .Companion .Created
96+
97+ // Second POST with different data (overwrite)
98+ val response2 = client.post("/versions") {
99+ contentType(ContentType .Application .Json )
100+ setBody(updatedVersion.toJsonString())
101+ header(HttpHeaders .Authorization , BasicAuthHeader )
102+ }
103+ response2.status shouldBe HttpStatusCode .Companion .Created
104+ }
105+ // Verify the updated version is stored
106+ selectVersion(
107+ candidate = updatedVersion.candidate,
108+ version = updatedVersion.version,
109+ vendor = updatedVersion.vendor,
110+ platform = updatedVersion.platform
111+ ) shouldBe updatedVersion.some()
112+ }
113+ }
114+
115+ should("POST be idempotent for version without vendor") {
116+ val version = Version (
117+ candidate = "scala",
118+ version = "3.2.0",
119+ platform = Platform .UNIVERSAL ,
120+ url = "https://scala-3.2.0",
121+ visible = true,
122+ vendor = None
123+ )
124+ val requestBody = version.toJsonString()
125+
126+ withCleanDatabase {
127+ withTestApplication {
128+ // First POST
129+ val response1 = client.post("/versions") {
130+ contentType(ContentType .Application .Json )
131+ setBody(requestBody)
132+ header(HttpHeaders .Authorization , BasicAuthHeader )
133+ }
134+ response1.status shouldBe HttpStatusCode .Companion .Created
135+
136+ // Second POST (idempotent)
137+ val response2 = client.post("/versions") {
138+ contentType(ContentType .Application .Json )
139+ setBody(requestBody)
140+ header(HttpHeaders .Authorization , BasicAuthHeader )
141+ }
142+ response2.status shouldBe HttpStatusCode .Companion .Created
143+ }
144+ // Verify version exists in database
145+ selectVersion(
146+ candidate = version.candidate,
147+ version = version.version,
148+ vendor = version.vendor,
149+ platform = version.platform
150+ ) shouldBe version.some()
151+ }
152+ }
153+ })
0 commit comments