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 = (0 until childNodes.length) .map(childNodes::item) .filterIsInstance()