implemented 03-gitea-client.md: added Gitea API client for commit statuses, status mapping, and extensive tests
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
package de.hoennig.gittally.gitea
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.aResponse
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.equalToJson
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.get
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.okJson
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.post
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.config.ConfigLoader
|
||||
import de.hoennig.gittally.config.GitConfig
|
||||
import de.hoennig.gittally.config.GitTallyConfig
|
||||
import de.hoennig.gittally.config.GiteaConfig
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class GiteaClientTest :
|
||||
FunSpec({
|
||||
|
||||
val server = WireMockServer(options().dynamicPort())
|
||||
val configLoader = mockk<ConfigLoader>()
|
||||
val client = GiteaClient(configLoader)
|
||||
|
||||
val publishUrl = "/api/v1/repos/the-owner/the-repo/statuses/abc123"
|
||||
val readUrl = "/api/v1/repos/the-owner/the-repo/commits/abc123/statuses?sort=recentupdate"
|
||||
|
||||
fun configure(
|
||||
baseUrl: String = "http://localhost:${server.port()}",
|
||||
owner: String = "the-owner",
|
||||
repo: String = "the-repo",
|
||||
token: String = "secret-token",
|
||||
) {
|
||||
every { configLoader.load(any()) } returns
|
||||
GitTallyConfig(
|
||||
git = GitConfig(token = token),
|
||||
gitea = GiteaConfig(baseUrl = baseUrl, owner = owner, repo = repo, statusContext = "GitTally"),
|
||||
)
|
||||
}
|
||||
|
||||
beforeSpec { server.start() }
|
||||
afterSpec { server.stop() }
|
||||
beforeEach {
|
||||
server.resetAll()
|
||||
configure()
|
||||
}
|
||||
|
||||
context("isEnabled") {
|
||||
test("is true when baseUrl, owner, repo, and token are configured") {
|
||||
client.isEnabled() shouldBe true
|
||||
}
|
||||
|
||||
test("is false when any required value is missing") {
|
||||
configure(baseUrl = "")
|
||||
client.isEnabled() shouldBe false
|
||||
configure(owner = "")
|
||||
client.isEnabled() shouldBe false
|
||||
configure(repo = "")
|
||||
client.isEnabled() shouldBe false
|
||||
configure(token = "")
|
||||
client.isEnabled() shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
context("publishStatus") {
|
||||
test("posts state, context, description, and target_url with token auth") {
|
||||
server.stubFor(post(publishUrl).willReturn(aResponse().withStatus(201)))
|
||||
|
||||
val published =
|
||||
client.publishStatus(
|
||||
sha = "abc123",
|
||||
status = BuildStatus.SUCCESS,
|
||||
description = "Build succeeded",
|
||||
targetUrl = "https://ci.example.org/branches/main/index.html",
|
||||
)
|
||||
|
||||
published shouldBe true
|
||||
server.verify(
|
||||
postRequestedFor(urlEqualTo(publishUrl))
|
||||
.withHeader("Authorization", equalTo("token secret-token"))
|
||||
.withHeader("Content-Type", equalTo("application/json"))
|
||||
.withRequestBody(
|
||||
equalToJson(
|
||||
"""
|
||||
{
|
||||
"state": "success",
|
||||
"context": "GitTally",
|
||||
"description": "Build succeeded",
|
||||
"target_url": "https://ci.example.org/branches/main/index.html"
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
test("omits target_url when none is given") {
|
||||
server.stubFor(post(publishUrl).willReturn(aResponse().withStatus(201)))
|
||||
|
||||
client.publishStatus("abc123", BuildStatus.RUNNING, "Build running") shouldBe true
|
||||
|
||||
server.verify(
|
||||
postRequestedFor(urlEqualTo(publishUrl))
|
||||
.withRequestBody(
|
||||
equalToJson("""{"state": "pending", "context": "GitTally", "description": "Build running"}"""),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
test("maps every build status to the documented Gitea state") {
|
||||
server.stubFor(post(publishUrl).willReturn(aResponse().withStatus(201)))
|
||||
val expectedStates =
|
||||
mapOf(
|
||||
BuildStatus.SUCCESS to "success",
|
||||
BuildStatus.FAILED to "failure",
|
||||
BuildStatus.INTERRUPTED to "failure",
|
||||
BuildStatus.CANCELLED to "failure",
|
||||
BuildStatus.PENDING to "pending",
|
||||
BuildStatus.RUNNING to "pending",
|
||||
)
|
||||
|
||||
expectedStates.forEach { (status, state) ->
|
||||
server.resetRequests()
|
||||
client.publishStatus("abc123", status, "d") shouldBe true
|
||||
server.verify(
|
||||
postRequestedFor(urlEqualTo(publishUrl))
|
||||
.withRequestBody(equalToJson("""{"state": "$state", "context": "GitTally", "description": "d"}""")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("returns false without a request when disabled") {
|
||||
configure(token = "")
|
||||
|
||||
client.publishStatus("abc123", BuildStatus.SUCCESS, "d") shouldBe false
|
||||
|
||||
server.findAll(postRequestedFor(urlEqualTo(publishUrl))).size shouldBe 0
|
||||
}
|
||||
|
||||
test("returns false on HTTP errors instead of throwing") {
|
||||
server.stubFor(post(publishUrl).willReturn(aResponse().withStatus(500)))
|
||||
|
||||
client.publishStatus("abc123", BuildStatus.SUCCESS, "d") shouldBe false
|
||||
}
|
||||
|
||||
test("returns false when Gitea is unreachable instead of throwing") {
|
||||
configure(baseUrl = "http://localhost:1")
|
||||
|
||||
client.publishStatus("abc123", BuildStatus.SUCCESS, "d") shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
context("readStatus") {
|
||||
test("requests statuses sorted by recentupdate with token auth") {
|
||||
server.stubFor(get(readUrl).willReturn(okJson("[]")))
|
||||
|
||||
client.readStatus("abc123")
|
||||
|
||||
server.verify(
|
||||
getRequestedFor(urlEqualTo(readUrl))
|
||||
.withHeader("Authorization", equalTo("token secret-token")),
|
||||
)
|
||||
}
|
||||
|
||||
test("returns the newest status matching the configured context") {
|
||||
server.stubFor(
|
||||
get(readUrl).willReturn(
|
||||
okJson(
|
||||
"""
|
||||
[
|
||||
{"context": "other-ci", "state": "failure"},
|
||||
{"context": "GitTally", "state": "success", "description": "Build succeeded"},
|
||||
{"context": "GitTally", "state": "pending"}
|
||||
]
|
||||
""".trimIndent(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
client.readStatus("abc123") shouldBe GiteaStatusResult.Found(BuildStatus.SUCCESS)
|
||||
}
|
||||
|
||||
test("returns None when no status matches the context") {
|
||||
server.stubFor(get(readUrl).willReturn(okJson("""[{"context": "other-ci", "state": "success"}]""")))
|
||||
|
||||
client.readStatus("abc123") shouldBe GiteaStatusResult.None
|
||||
}
|
||||
|
||||
test("returns None for an empty status list") {
|
||||
server.stubFor(get(readUrl).willReturn(okJson("[]")))
|
||||
|
||||
client.readStatus("abc123") shouldBe GiteaStatusResult.None
|
||||
}
|
||||
|
||||
test("returns Error for malformed JSON instead of throwing") {
|
||||
server.stubFor(get(readUrl).willReturn(okJson("""{"oops": "not a list"!!!""")))
|
||||
|
||||
client.readStatus("abc123").shouldBeInstanceOf<GiteaStatusResult.Error>()
|
||||
}
|
||||
|
||||
test("returns Error for an unknown state value") {
|
||||
server.stubFor(get(readUrl).willReturn(okJson("""[{"context": "GitTally", "state": "hovering"}]""")))
|
||||
|
||||
client.readStatus("abc123").shouldBeInstanceOf<GiteaStatusResult.Error>()
|
||||
}
|
||||
|
||||
test("returns Error on HTTP 4xx/5xx instead of throwing") {
|
||||
server.stubFor(get(readUrl).willReturn(aResponse().withStatus(404)))
|
||||
client.readStatus("abc123").shouldBeInstanceOf<GiteaStatusResult.Error>()
|
||||
|
||||
server.stubFor(get(readUrl).willReturn(aResponse().withStatus(500)))
|
||||
client.readStatus("abc123").shouldBeInstanceOf<GiteaStatusResult.Error>()
|
||||
}
|
||||
|
||||
test("returns Error when Gitea is unreachable instead of throwing") {
|
||||
configure(baseUrl = "http://localhost:1")
|
||||
|
||||
client.readStatus("abc123").shouldBeInstanceOf<GiteaStatusResult.Error>()
|
||||
}
|
||||
|
||||
test("returns Disabled when unconfigured, without a request") {
|
||||
configure(owner = "")
|
||||
|
||||
client.readStatus("abc123") shouldBe GiteaStatusResult.Disabled
|
||||
|
||||
server.findAll(getRequestedFor(urlEqualTo(readUrl))).size shouldBe 0
|
||||
}
|
||||
}
|
||||
|
||||
context("resolveUsername") {
|
||||
test("returns the login of the token's user") {
|
||||
server.stubFor(get("/api/v1/user").willReturn(okJson("""{"id": 42, "login": "the-user"}""")))
|
||||
|
||||
client.resolveUsername() shouldBe "the-user"
|
||||
|
||||
server.verify(
|
||||
getRequestedFor(urlEqualTo("/api/v1/user"))
|
||||
.withHeader("Authorization", equalTo("token secret-token")),
|
||||
)
|
||||
}
|
||||
|
||||
test("only needs baseUrl and token, not owner/repo") {
|
||||
configure(owner = "", repo = "")
|
||||
server.stubFor(get("/api/v1/user").willReturn(okJson("""{"login": "the-user"}""")))
|
||||
|
||||
client.resolveUsername() shouldBe "the-user"
|
||||
}
|
||||
|
||||
test("returns null when baseUrl or token is missing") {
|
||||
configure(baseUrl = "")
|
||||
client.resolveUsername().shouldBeNull()
|
||||
|
||||
configure(token = "")
|
||||
client.resolveUsername().shouldBeNull()
|
||||
}
|
||||
|
||||
test("returns null on HTTP errors, missing login, or malformed JSON instead of throwing") {
|
||||
server.stubFor(get("/api/v1/user").willReturn(aResponse().withStatus(401)))
|
||||
client.resolveUsername().shouldBeNull()
|
||||
|
||||
server.stubFor(get("/api/v1/user").willReturn(okJson("""{"id": 42}""")))
|
||||
client.resolveUsername().shouldBeNull()
|
||||
|
||||
server.stubFor(get("/api/v1/user").willReturn(okJson("not json")))
|
||||
client.resolveUsername().shouldBeNull()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.hoennig.gittally.gitea
|
||||
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
|
||||
class GiteaStateMappingTest :
|
||||
FunSpec({
|
||||
|
||||
test("maps each build status to its Gitea state") {
|
||||
BuildStatus.SUCCESS.toGiteaState() shouldBe "success"
|
||||
BuildStatus.FAILED.toGiteaState() shouldBe "failure"
|
||||
BuildStatus.INTERRUPTED.toGiteaState() shouldBe "failure"
|
||||
BuildStatus.CANCELLED.toGiteaState() shouldBe "failure"
|
||||
BuildStatus.PENDING.toGiteaState() shouldBe "pending"
|
||||
BuildStatus.RUNNING.toGiteaState() shouldBe "pending"
|
||||
}
|
||||
|
||||
test("maps Gitea states back to build statuses") {
|
||||
buildStatusFromGiteaState("success") shouldBe BuildStatus.SUCCESS
|
||||
buildStatusFromGiteaState("failure") shouldBe BuildStatus.FAILED
|
||||
buildStatusFromGiteaState("error") shouldBe BuildStatus.FAILED
|
||||
buildStatusFromGiteaState("warning") shouldBe BuildStatus.FAILED
|
||||
buildStatusFromGiteaState("pending") shouldBe BuildStatus.RUNNING
|
||||
}
|
||||
|
||||
test("maps unknown Gitea states to null") {
|
||||
buildStatusFromGiteaState("").shouldBeNull()
|
||||
buildStatusFromGiteaState("deleted").shouldBeNull()
|
||||
buildStatusFromGiteaState("SUCCESS").shouldBeNull()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user