Files
OSGAccountServer/src/test/kotlin/com/osglab/account/config/LogbackConfigurationTest.kt
T
Rocky 405a2cfc0f Harden admin deployment and local acceptance
Enforce mTLS and least-privilege runtime boundaries while adding repeatable MySQL 8.4 and Docker smoke checks that require no production secrets.
2026-08-17 15:20:46 +08:00

74 lines
2.8 KiB
Kotlin

package com.osglab.account.config
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator
import ch.qos.logback.core.status.Status
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import java.nio.file.Path
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element
import org.w3c.dom.Node
class LogbackConfigurationTest : FunSpec({
val logbackPath = Path.of(System.getProperty("user.dir"), "src/main/resources/logback.xml")
test("logback configuration is parseable and has one root") {
val document = secureDocumentBuilderFactory()
.newDocumentBuilder()
.parse(logbackPath.toFile())
document.documentElement.tagName shouldBe "configuration"
document.getElementsByTagName("configuration").length shouldBe 1
document.childElements().map(Element::getTagName) shouldContainExactly listOf("configuration")
}
test("console pattern is single-line and contains required structured fields") {
val document = secureDocumentBuilderFactory()
.newDocumentBuilder()
.parse(logbackPath.toFile())
val patterns = document.getElementsByTagName("pattern")
patterns.length shouldBe 1
val pattern = patterns.item(0).textContent.trim()
pattern.lines().size shouldBe 1
listOf("time=", "level=", "logger=", "requestId=", "message=").forEach(pattern::shouldContain)
pattern shouldContain "[REDACTED]"
pattern shouldContain "%nopex"
}
test("logback accepts the structured pattern without configuration errors") {
val context = LoggerContext()
try {
JoranConfigurator().apply { this.context = context }.doConfigure(logbackPath.toFile())
context.statusManager.copyOfStatusList
.filter { it.level == Status.ERROR }
.shouldBeEmpty()
} finally {
context.stop()
}
}
})
private fun secureDocumentBuilderFactory(): DocumentBuilderFactory =
DocumentBuilderFactory.newInstance().apply {
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
setFeature("http://xml.org/sax/features/external-general-entities", false)
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "")
setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")
isXIncludeAware = false
isExpandEntityReferences = false
}
private fun Node.childElements(): List<Element> =
(0 until childNodes.length)
.map(childNodes::item)
.filterIsInstance<Element>()