The artifact page read `branches.<branch>.buildCommand` from the primary config only, so for a named build on a branch with its own config it showed a command that build never ran — on vm4006 it showed the host config's command for a build that ran the branch definition's `pitestFull`. Resolving "what does this build run" now has one implementation, `GitTallyConfig.buildSettings(branch, build)`: the branch entry with the build definition's overrides applied last. The executor uses it, and the page resolves it against the branch layer committed at the build's own commit — the same inputs the executor had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
529 lines
25 KiB
Kotlin
529 lines
25 KiB
Kotlin
package de.hoennig.gittally.server
|
|
|
|
import com.ninjasquad.springmockk.MockkBean
|
|
import de.hoennig.gittally.build.ArtifactStore
|
|
import de.hoennig.gittally.build.BuildExecutor
|
|
import de.hoennig.gittally.build.BuildResult
|
|
import de.hoennig.gittally.build.BuildResultRepository
|
|
import de.hoennig.gittally.build.BuildStatus
|
|
import de.hoennig.gittally.build.RunningBuild
|
|
import de.hoennig.gittally.config.BranchConfig
|
|
import de.hoennig.gittally.config.BuildDefinition
|
|
import de.hoennig.gittally.config.ConfigLoader
|
|
import de.hoennig.gittally.config.GitTallyConfig
|
|
import de.hoennig.gittally.config.GiteaConfig
|
|
import de.hoennig.gittally.config.ServerConfig
|
|
import de.hoennig.gittally.git.GitService
|
|
import de.hoennig.gittally.metrics.MetricAggregate
|
|
import de.hoennig.gittally.metrics.SystemMetrics
|
|
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.string.shouldContain
|
|
import io.kotest.matchers.string.shouldNotContain
|
|
import io.mockk.clearMocks
|
|
import io.mockk.every
|
|
import org.hamcrest.Matchers.containsString
|
|
import org.hamcrest.Matchers.not
|
|
import org.springframework.beans.factory.annotation.Autowired
|
|
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
|
|
import org.springframework.http.HttpStatus
|
|
import org.springframework.test.web.servlet.MockMvc
|
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
|
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
|
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
|
import org.springframework.web.server.ResponseStatusException
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
import java.time.Duration
|
|
import java.time.Instant
|
|
|
|
@WebMvcTest(UiController::class, properties = ["spring.main.web-application-type=servlet"])
|
|
class UiControllerTest : FunSpec() {
|
|
private val tempDir: Path = Files.createTempDirectory("gittally-ui-test")
|
|
|
|
@Autowired
|
|
lateinit var mockMvc: MockMvc
|
|
|
|
@MockkBean
|
|
lateinit var repository: BuildResultRepository
|
|
|
|
@MockkBean
|
|
lateinit var buildExecutor: BuildExecutor
|
|
|
|
@MockkBean
|
|
lateinit var artifactStore: ArtifactStore
|
|
|
|
@MockkBean
|
|
lateinit var controlTokens: ControlTokenService
|
|
|
|
@MockkBean
|
|
lateinit var configLoader: ConfigLoader
|
|
|
|
@MockkBean
|
|
lateinit var gitService: GitService
|
|
|
|
@MockkBean
|
|
lateinit var metricsCollector: SystemMetricsCollector
|
|
|
|
@MockkBean
|
|
lateinit var branchListing: BranchListing
|
|
|
|
@MockkBean
|
|
lateinit var branchPermalinks: BranchPermalinks
|
|
|
|
private val startedAt = Instant.parse("2026-07-07T10:00:00Z")
|
|
|
|
private val emptySystemMetrics =
|
|
SystemMetrics(
|
|
timestamp = null,
|
|
sampleCount = 0,
|
|
cpuCount = 8,
|
|
ramTotalGib = null,
|
|
diskTotalGib = null,
|
|
cpuUsed = null,
|
|
cpuIdle = null,
|
|
ramUsedGib = null,
|
|
ramFreeGib = null,
|
|
diskUsedGib = null,
|
|
diskFreeGib = null,
|
|
repoSizeGib = null,
|
|
)
|
|
|
|
private val successResult =
|
|
BuildResult(
|
|
branch = "main",
|
|
commit = "0123456789abcdef0123456789abcdef01234567",
|
|
status = BuildStatus.SUCCESS,
|
|
startedAt = startedAt,
|
|
duration = Duration.ofSeconds(83),
|
|
artifactKey = "main-abc123-key",
|
|
)
|
|
|
|
init {
|
|
beforeEach {
|
|
clearMocks(
|
|
repository,
|
|
buildExecutor,
|
|
artifactStore,
|
|
controlTokens,
|
|
configLoader,
|
|
gitService,
|
|
metricsCollector,
|
|
branchListing,
|
|
branchPermalinks,
|
|
)
|
|
every { configLoader.load(any()) } returns
|
|
GitTallyConfig(
|
|
server = ServerConfig(impressumUrl = "https://example.org/imprint"),
|
|
gitea = GiteaConfig(baseUrl = "https://git.example.org", owner = "acme", repo = "widget"),
|
|
)
|
|
every { configLoader.loadWithBranchLayer(any(), anyNullable()) } returns GitTallyConfig()
|
|
every { gitService.showFileAtCommit(any(), any(), any()) } returns null
|
|
every { controlTokens.token() } returns "test-token"
|
|
every { repository.latestGreenFor(any()) } returns null
|
|
}
|
|
|
|
test("latest view renders the empty state, and the nav no longer offers the current view") {
|
|
every { repository.latestPerName() } returns emptyList()
|
|
|
|
mockMvc
|
|
.perform(get("/"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("No builds recorded yet.")))
|
|
.andExpect(content().string(containsString("""data-api="/api/builds/latest"""")))
|
|
.andExpect(content().string(containsString("""id="reload-button"""")))
|
|
.andExpect(content().string(not(containsString("""href="/current""""))))
|
|
}
|
|
|
|
test("latest view renders rows with badge, Gitea links, artifact link, actions, and token") {
|
|
every { repository.latestPerName() } returns listOf(successResult)
|
|
|
|
mockMvc
|
|
.perform(get("/"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("""status status-success""")))
|
|
.andExpect(content().string(containsString("https://git.example.org/acme/widget/src/branch/main")))
|
|
.andExpect(
|
|
content().string(containsString("https://git.example.org/acme/widget/commit/0123456789abcdef0123456789abcdef01234567")),
|
|
).andExpect(content().string(containsString("/builds/main-abc123-key")))
|
|
.andExpect(content().string(containsString("""data-action="restart"""")))
|
|
.andExpect(content().string(containsString("""data-action="delete"""")))
|
|
// the control token must never reach the browser: reading a page is unauthenticated
|
|
.andExpect(content().string(not(containsString("gittally-control-token"))))
|
|
.andExpect(content().string(not(containsString("test-token"))))
|
|
.andExpect(content().string(containsString("https://example.org/imprint")))
|
|
.andExpect(content().string(containsString("1:23")))
|
|
}
|
|
|
|
test("branches view renders built and never-built branches with restart actions") {
|
|
every { branchListing.branches(any()) } returns
|
|
listOf(
|
|
BranchDto.from("main", "ignored-head", successResult, isLatestGreen = true),
|
|
BranchDto.from("feature/x", "fedcba9876543210fedcba9876543210fedcba98", null),
|
|
)
|
|
|
|
mockMvc
|
|
.perform(get("/branches"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("status status-success")))
|
|
.andExpect(content().string(containsString("status status-unknown")))
|
|
.andExpect(content().string(containsString("feature/x")))
|
|
.andExpect(content().string(containsString("fedcba987654")))
|
|
.andExpect(content().string(containsString("""data-api="/api/branches"""")))
|
|
.andExpect(content().string(containsString("""data-action="restart"""")))
|
|
.andExpect(content().string(containsString("""href="/branches/main"""")))
|
|
.andExpect(content().string(containsString("Permanent link")))
|
|
}
|
|
|
|
test("the permanent link shows on the branch's latest green build only, the live link while it runs") {
|
|
val running = successResult.copy(status = BuildStatus.RUNNING, duration = null, artifactKey = "running-key")
|
|
every { repository.history() } returns listOf(running, successResult)
|
|
every { repository.latestGreenFor("main") } returns successResult
|
|
|
|
val page =
|
|
mockMvc
|
|
.perform(get("/history"))
|
|
.andExpect(status().isOk)
|
|
.andReturn()
|
|
.response.contentAsString
|
|
|
|
Regex("""href="/branches/main"""").findAll(page).count() shouldBe 1
|
|
Regex("""href="/current"""").findAll(page).count() shouldBe 1
|
|
page shouldContain "Watch this build live"
|
|
}
|
|
|
|
test("history view renders mixed history without restart actions") {
|
|
every { repository.history() } returns
|
|
listOf(
|
|
successResult.copy(
|
|
branch = "main",
|
|
name = "main",
|
|
status = BuildStatus.RUNNING,
|
|
duration = null,
|
|
artifactKey = "run-key",
|
|
),
|
|
successResult,
|
|
successResult.copy(branch = "feature/x", name = "feature/x", status = BuildStatus.FAILED, artifactKey = "failed-key"),
|
|
)
|
|
|
|
mockMvc
|
|
.perform(get("/history"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("status status-running")))
|
|
.andExpect(content().string(containsString("status status-success")))
|
|
.andExpect(content().string(containsString("status status-failed")))
|
|
.andExpect(content().string(containsString("feature/x")))
|
|
.andExpect(content().string(not(containsString("""data-action="restart""""))))
|
|
}
|
|
|
|
test("current view renders a card per running build with cancel button and started-at attribute") {
|
|
val build =
|
|
RunningBuild(
|
|
branch = "main",
|
|
commit = successResult.commit,
|
|
artifactKey = "main-abc123-running",
|
|
startedAt = startedAt,
|
|
stagingDir = tempDir,
|
|
liveLogFile = tempDir.resolve("build.log"),
|
|
)
|
|
every { buildExecutor.currentBuilds() } returns listOf(build)
|
|
every { repository.history() } returns
|
|
listOf(successResult.copy(status = BuildStatus.RUNNING, artifactKey = build.artifactKey, duration = null))
|
|
|
|
mockMvc
|
|
.perform(get("/current"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("build-card")))
|
|
.andExpect(content().string(containsString("""data-started-at="2026-07-07T10:00:00Z"""")))
|
|
.andExpect(content().string(containsString("""data-action="cancel"""")))
|
|
.andExpect(content().string(containsString("status status-running")))
|
|
}
|
|
|
|
test("current view renders a clear no-build state") {
|
|
every { buildExecutor.currentBuilds() } returns emptyList()
|
|
every { repository.history() } returns emptyList()
|
|
|
|
mockMvc
|
|
.perform(get("/current"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("No build is currently running.")))
|
|
}
|
|
|
|
test("artifact index renders build command, log links, and topmost report index pages") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key"))
|
|
Files.writeString(artifactDir.resolve("build.stdout.log"), "out")
|
|
Files.writeString(artifactDir.resolve("build.stderr.log"), "err")
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/test"))
|
|
Files.writeString(artifactDir.resolve("reports/tests/test/index.html"), "<html></html>")
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/test/packages"))
|
|
Files.writeString(artifactDir.resolve("reports/tests/test/packages/index.html"), "<html></html>")
|
|
every { repository.history() } returns listOf(successResult)
|
|
every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir
|
|
|
|
mockMvc
|
|
.perform(get("/builds/main-abc123-key"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("./gradlew --console=plain --no-daemon test")))
|
|
.andExpect(content().string(containsString("""/artifacts/main-abc123-key/build.stdout.log" target="_blank"""")))
|
|
.andExpect(content().string(containsString("/artifacts/main-abc123-key/build.stderr.log")))
|
|
.andExpect(
|
|
content().string(containsString("""/artifacts/main-abc123-key/reports/tests/test/index.html" target="_blank"""")),
|
|
).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html"))))
|
|
}
|
|
|
|
test("the artifact page shows the command of the build's own definition, not the plain branch command") {
|
|
val pitestResult =
|
|
successResult.copy(
|
|
build = "pitest",
|
|
name = "main@pitest",
|
|
artifactKey = "main-pitest-key",
|
|
)
|
|
every { repository.history() } returns listOf(pitestResult)
|
|
every { artifactStore.artifactDir("main-pitest-key") } returns null
|
|
every { configLoader.loadWithBranchLayer(any(), anyNullable()) } returns
|
|
GitTallyConfig(
|
|
branches = mapOf("default" to BranchConfig(buildCommand = "./gradlew quick-check")),
|
|
buildDefinitions = mapOf("pitest" to BuildDefinition(buildCommand = "./gradlew pitestFull")),
|
|
)
|
|
|
|
mockMvc
|
|
.perform(get("/builds/main-pitest-key"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("./gradlew pitestFull")))
|
|
.andExpect(content().string(not(containsString("./gradlew quick-check"))))
|
|
}
|
|
|
|
test("artifact index links a single index-less report page as a directory, keeping the URL stable") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key"))
|
|
Files.createDirectories(artifactDir.resolve("reports/profile"))
|
|
Files.writeString(artifactDir.resolve("reports/profile/profile-2026-08-10-18-36-12.html"), "<html></html>")
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/test"))
|
|
Files.writeString(artifactDir.resolve("reports/tests/test/index.html"), "<html></html>")
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/test/classes"))
|
|
Files.writeString(artifactDir.resolve("reports/tests/test/classes/SomeTest.html"), "<html></html>")
|
|
every { repository.history() } returns listOf(successResult)
|
|
every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir
|
|
|
|
val page =
|
|
mockMvc
|
|
.perform(get("/builds/main-abc123-key"))
|
|
.andExpect(status().isOk)
|
|
.andReturn()
|
|
.response.contentAsString
|
|
|
|
page shouldContain "reports/profile/"
|
|
page shouldNotContain "profile-2026-08-10-18-36-12.html"
|
|
page shouldContain "reports/tests/test/index.html"
|
|
page shouldNotContain "SomeTest.html"
|
|
}
|
|
|
|
test("artifact index links the pages of an index-less report directory holding several") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key"))
|
|
Files.createDirectories(artifactDir.resolve("reports/pmd"))
|
|
Files.writeString(artifactDir.resolve("reports/pmd/main.html"), "<html></html>")
|
|
Files.writeString(artifactDir.resolve("reports/pmd/test.html"), "<html></html>")
|
|
every { repository.history() } returns listOf(successResult)
|
|
every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir
|
|
|
|
val page =
|
|
mockMvc
|
|
.perform(get("/builds/main-abc123-key"))
|
|
.andExpect(status().isOk)
|
|
.andReturn()
|
|
.response.contentAsString
|
|
|
|
page shouldContain "reports/pmd/main.html"
|
|
page shouldContain "reports/pmd/test.html"
|
|
}
|
|
|
|
test("report links carry a failed-badge from the report's failures counter") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key"))
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/test"))
|
|
Files.writeString(
|
|
artifactDir.resolve("reports/tests/test/index.html"),
|
|
"""<div class="infoBox" id="failures">
|
|
<div class="counter">2</div>
|
|
<p>failures</p></div>""",
|
|
)
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/unitTest"))
|
|
Files.writeString(
|
|
artifactDir.resolve("reports/tests/unitTest/index.html"),
|
|
"""<div class="infoBox" id="failures">
|
|
<div class="counter">0</div>
|
|
<p>failures</p></div>""",
|
|
)
|
|
Files.createDirectories(artifactDir.resolve("reports/jacoco"))
|
|
Files.writeString(artifactDir.resolve("reports/jacoco/index.html"), "<html>no counter</html>")
|
|
every { repository.history() } returns listOf(successResult)
|
|
every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir
|
|
|
|
val page =
|
|
mockMvc
|
|
.perform(get("/builds/main-abc123-key"))
|
|
.andExpect(status().isOk)
|
|
.andReturn()
|
|
.response.contentAsString
|
|
|
|
page shouldContain "2 failed"
|
|
page shouldNotContain "0 failed"
|
|
// exactly one badge: the report with failures, none for the clean or counter-less reports
|
|
Regex(""" failed</span>""").findAll(page).count() shouldBe 1
|
|
}
|
|
|
|
test("of a failed build, only the logs carrying a failure line get a failed-badge") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("failed-key"))
|
|
Files.writeString(artifactDir.resolve("build.log"), "compiling\nBUILD FAILED in 20s\n")
|
|
// a failed test, far away from any BUILD FAILED line
|
|
Files.writeString(artifactDir.resolve("build.stdout.log"), "SomeTest > works() FAILED\ncompiling\n")
|
|
Files.writeString(artifactDir.resolve("build.stderr.log"), "warning: this test has failed before\n")
|
|
every { repository.history() } returns
|
|
listOf(successResult.copy(status = BuildStatus.FAILED, artifactKey = "failed-key"))
|
|
every { artifactStore.artifactDir("failed-key") } returns artifactDir
|
|
|
|
val page =
|
|
mockMvc
|
|
.perform(get("/builds/failed-key"))
|
|
.andExpect(status().isOk)
|
|
.andReturn()
|
|
.response.contentAsString
|
|
|
|
// two badges for the logs, plus the one of the build status itself
|
|
Regex(""">failed</span>""").findAll(page).count() shouldBe 3
|
|
// lower-case prose about failing is not a failure line
|
|
page.substringAfter("build.stderr.log</a>").substringBefore("</li>") shouldNotContain "status-failed"
|
|
}
|
|
|
|
test("logs of a successful build are not scanned for failure lines") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("green-key"))
|
|
Files.writeString(artifactDir.resolve("build.log"), "BUILD FAILED in a nested build\nBUILD SUCCESSFUL\n")
|
|
every { repository.history() } returns listOf(successResult.copy(artifactKey = "green-key"))
|
|
every { artifactStore.artifactDir("green-key") } returns artifactDir
|
|
|
|
val page =
|
|
mockMvc
|
|
.perform(get("/builds/green-key"))
|
|
.andExpect(status().isOk)
|
|
.andReturn()
|
|
.response.contentAsString
|
|
|
|
page shouldContain "build.log"
|
|
page shouldNotContain "status-failed"
|
|
}
|
|
|
|
test("legacy page names redirect permanently to the new routes") {
|
|
mockMvc
|
|
.perform(get("/index.html"))
|
|
.andExpect(status().isMovedPermanently)
|
|
.andExpect(header().string("Location", "/"))
|
|
mockMvc
|
|
.perform(get("/branches.html"))
|
|
.andExpect(status().isMovedPermanently)
|
|
.andExpect(header().string("Location", "/branches"))
|
|
mockMvc
|
|
.perform(get("/history.html"))
|
|
.andExpect(status().isMovedPermanently)
|
|
.andExpect(header().string("Location", "/history"))
|
|
}
|
|
|
|
test("release notes page renders the version history and the footer links to it") {
|
|
mockMvc
|
|
.perform(get("/releases"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("Release Notes")))
|
|
.andExpect(content().string(containsString("v0.9.0")))
|
|
.andExpect(content().string(containsString("Kotlin/Spring Boot")))
|
|
// the footer version (rendered on every page) is the link to this page
|
|
.andExpect(content().string(containsString("""href="/releases"""")))
|
|
}
|
|
|
|
test("artifact index of a pruned build explains the missing artifacts") {
|
|
every { repository.history() } returns listOf(successResult)
|
|
every { artifactStore.artifactDir("main-abc123-key") } returns null
|
|
|
|
mockMvc
|
|
.perform(get("/builds/main-abc123-key"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("No log files are stored for this build")))
|
|
}
|
|
|
|
test("permanent artifact index renders the latest green build with permanent file links") {
|
|
val artifactDir = Files.createDirectories(tempDir.resolve("permanent-main-key"))
|
|
Files.writeString(artifactDir.resolve("build.stdout.log"), "out")
|
|
Files.createDirectories(artifactDir.resolve("reports/tests/test"))
|
|
Files.writeString(artifactDir.resolve("reports/tests/test/index.html"), "<html></html>")
|
|
every { branchPermalinks.latestGreenBuild("main") } returns successResult
|
|
every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir
|
|
|
|
mockMvc
|
|
.perform(get("/branches/main"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("latest green build of branch")))
|
|
.andExpect(content().string(containsString("""/branches/main/build.stdout.log" target="_blank"""")))
|
|
.andExpect(content().string(containsString("""/branches/main/reports/tests/test/index.html"""")))
|
|
.andExpect(content().string(containsString("/builds/main-abc123-key")))
|
|
.andExpect(content().string(not(containsString("/artifacts/main-abc123-key"))))
|
|
}
|
|
|
|
test("permanent artifact index of a branch without a green build answers 404") {
|
|
every { branchPermalinks.latestGreenBuild("main") } throws
|
|
ResponseStatusException(HttpStatus.NOT_FOUND, "branch 'main' has no successful build")
|
|
|
|
mockMvc
|
|
.perform(get("/branches/main"))
|
|
.andExpect(status().isNotFound)
|
|
}
|
|
|
|
test("artifact index of an unknown key answers 404") {
|
|
every { repository.history() } returns emptyList()
|
|
every { artifactStore.artifactDir("no-such-key") } returns null
|
|
|
|
mockMvc
|
|
.perform(get("/builds/no-such-key"))
|
|
.andExpect(status().isNotFound)
|
|
}
|
|
|
|
test("system view renders metric rows, totals, and the polling hook") {
|
|
every { metricsCollector.snapshot() } returns
|
|
emptySystemMetrics.copy(
|
|
timestamp = Instant.parse("2026-07-07T10:00:00Z"),
|
|
sampleCount = 5,
|
|
ramTotalGib = 32.0,
|
|
cpuUsed = MetricAggregate(current = 1.0, min = 0.5, max = 2.0, avg = 1.25),
|
|
)
|
|
|
|
mockMvc
|
|
.perform(get("/system"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("""data-api="/api/system"""")))
|
|
.andExpect(content().string(containsString("""data-metric="cpuUsed"""")))
|
|
.andExpect(content().string(containsString("CPU used (cores)")))
|
|
.andExpect(content().string(containsString("1.25")))
|
|
.andExpect(content().string(containsString("8 cores")))
|
|
.andExpect(content().string(containsString("32.00 GiB")))
|
|
}
|
|
|
|
test("system view renders n/a for unavailable metrics") {
|
|
every { metricsCollector.snapshot() } returns emptySystemMetrics
|
|
|
|
mockMvc
|
|
.perform(get("/system"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(containsString("Repo size (GiB)")))
|
|
.andExpect(content().string(containsString("n/a")))
|
|
}
|
|
|
|
test("branch names with HTML metacharacters render escaped") {
|
|
val nasty = "feat/<script>alert('x')</script>"
|
|
every { repository.latestPerName() } returns listOf(successResult.copy(branch = nasty, name = nasty))
|
|
|
|
mockMvc
|
|
.perform(get("/"))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().string(not(containsString("<script>alert"))))
|
|
.andExpect(content().string(containsString("<script>")))
|
|
}
|
|
}
|
|
}
|