fix(backend): Neustart gegen die eigene Datenbank, /info als Lebendprobe
Der Dienst lief einmal und stuerzte danach in einer Schleife: MODE=PostgreSQL laesst H2 unquotierte Bezeichner klein anlegen, Liquibase sucht seine Verwaltungstabellen gross, findet nichts, legt sie an — "Table databasechangelog already exists". Der erste Start ging, jeder weitere nicht. Gemessen mit dem echten Jar: ohne den Modus laufen beide. Liquibase auf Kleinschreibung zu konfigurieren half nicht, es korrigiert den Namen selbst zurueck — deshalb weicht der Modus ganz. Die Testsuite konnte das nicht finden (jeder Test bekommt eine frische In-Memory-DB), und der Regressionstest dafuer hat zweimal gelogen: erst reichte er die URL als Default-Property herein, die die application.yaml ueberstimmt; dann als Argument, aber damit pruefte er eine URL, die er sich selbst ausgedacht hatte. Jetzt hat die URL einen Regler (werkbaum.data-dir), der Test ueberschreibt nur den, und die Gegenprobe faellt. Dieselbe Sorte Fehler eine Ebene hoeher: Die Testkonfiguration hiess application.yaml und verdeckte damit die Hauptkonfiguration vollstaendig. Sie ist jetzt eine Profil-Ueberlagerung. Dazu GET /api/v1/info mit Name, Version und Bauzeitpunkt. Die Lebendprobe erwartete bisher eine 404 von einem Dokument, das es nicht gibt — ein erwarteter Fehler ist eine schlechte Zusicherung, dieselbe 404 liefert auch ein falsch konfigurierter Proxy. 138 Backend-Tests. Gegenprobe: MODE=PostgreSQL zurueck -> genau der Neustart-Test faellt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
78901793f0
commit
dea904814b
@@ -10,6 +10,7 @@ import de.werkbaum.generated.model.DocumentCreateRequest
|
||||
import de.werkbaum.generated.model.DocumentHistoryEntry as ApiHistoryEntry
|
||||
import de.werkbaum.generated.model.DocumentUpdateRequest
|
||||
import de.werkbaum.generated.model.RestoreRequest
|
||||
import de.werkbaum.generated.model.ServiceInfo
|
||||
import de.werkbaum.domain.ChangeAuthor
|
||||
import de.werkbaum.domain.ChangeEvent
|
||||
import de.werkbaum.domain.ChangeFeed
|
||||
@@ -18,6 +19,7 @@ import de.werkbaum.domain.Document
|
||||
import de.werkbaum.domain.DocumentHistoryEntry
|
||||
import de.werkbaum.service.DocumentService
|
||||
import de.werkbaum.service.LiveEditingService
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
import org.springframework.http.CacheControl
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
@@ -36,8 +38,30 @@ import java.util.UUID
|
||||
class DocumentsController(
|
||||
private val service: DocumentService,
|
||||
private val liveEditing: LiveEditingService,
|
||||
/**
|
||||
* Aus `META-INF/build-info.properties` (Gradle: `springBoot { buildInfo() }`).
|
||||
* Bewusst optional: Wer die Anwendung aus der IDE startet, hat die Datei
|
||||
* nicht — dann fehlt die Zusatzangabe, statt dass der Start scheitert.
|
||||
*/
|
||||
private val buildProperties: BuildProperties? = null,
|
||||
) : DocumentsApi {
|
||||
|
||||
/**
|
||||
* Lebendprobe: läuft der Dienst, und welcher Stand ist es?
|
||||
*
|
||||
* Offen und ohne Nebenwirkung. Vorher musste die Prüfung eine Anfrage nach
|
||||
* einem nicht existierenden Dokument stellen und auf **404** hoffen — ein
|
||||
* erwarteter Fehler ist eine schlechte Zusicherung, weil ihn auch ein
|
||||
* falsch konfigurierter Proxy liefert.
|
||||
*/
|
||||
override fun getInfo(): ResponseEntity<ServiceInfo> = ResponseEntity.ok(
|
||||
ServiceInfo(
|
||||
name = buildProperties?.name ?: "werkbaum-backend",
|
||||
version = buildProperties?.version ?: "unbekannt",
|
||||
builtAt = buildProperties?.time?.atOffset(java.time.ZoneOffset.UTC),
|
||||
)
|
||||
)
|
||||
|
||||
override fun listDocuments(): ResponseEntity<List<ApiDocument>> =
|
||||
ResponseEntity.ok(service.findAll().map { it.toApi() })
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ spring:
|
||||
name: editor-backend
|
||||
|
||||
datasource:
|
||||
# H2 im File-Modus mit PostgreSQL-Kompatibilitaet.
|
||||
# Spaeterer Umstieg auf echtes PostgreSQL = im Wesentlichen nur diese URL aendern.
|
||||
url: jdbc:h2:file:./data/editor;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
|
||||
# H2 im File-Modus. Bewusst OHNE MODE=PostgreSQL, obwohl das naheliegt:
|
||||
# In dem Modus legt H2 unquotierte Bezeichner klein an, Liquibase sucht
|
||||
# seine Verwaltungstabellen aber gross - findet nichts, legt sie an, und H2
|
||||
# antwortet "Table databasechangelog already exists". Folge: Der erste
|
||||
# Start geht, JEDER WEITERE stuerzt ab. Gemessen und in D77 begruendet;
|
||||
# der Umstieg auf echtes PostgreSQL bleibt eine Frage von URL und Treiber.
|
||||
url: jdbc:h2:file:${werkbaum.data-dir:./data}/editor;DEFAULT_NULL_ORDERING=HIGH
|
||||
username: sa
|
||||
password: ""
|
||||
driver-class-name: org.h2.Driver
|
||||
|
||||
@@ -313,6 +313,25 @@ paths:
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
/info:
|
||||
get:
|
||||
tags: [Documents]
|
||||
operationId: getInfo
|
||||
summary: Name und Version des Dienstes
|
||||
description: >
|
||||
Offen und ohne Nebenwirkung - gedacht als Lebendprobe fuer Deploy und
|
||||
Ueberwachung. Ohne diesen Endpunkt bliebe dafuer nur eine Anfrage nach
|
||||
einem Dokument, das es nicht gibt, und man muesste auf eine 404 hoffen:
|
||||
Ein erwarteter Fehler ist eine schlechte Zusicherung, weil ihn auch ein
|
||||
falsch konfigurierter Proxy liefert.
|
||||
responses:
|
||||
"200":
|
||||
description: Der Dienst laeuft
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServiceInfo"
|
||||
|
||||
components:
|
||||
responses:
|
||||
NotFound:
|
||||
@@ -565,6 +584,19 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/ChangeEvent"
|
||||
|
||||
ServiceInfo:
|
||||
type: object
|
||||
required: [name, version]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
builtAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Fehlt, wenn ohne Build-Informationen gestartet (z. B. aus der IDE).
|
||||
|
||||
ProblemDetail:
|
||||
type: object
|
||||
description: Fehlerformat nach RFC 9457 (Problem Details)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.werkbaum
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Die Anwendung muss **gegen ihre eigene Datenbank neu starten** können.
|
||||
*
|
||||
* Klingt selbstverständlich und war es nicht: `DATABASE_TO_LOWER=TRUE` lässt
|
||||
* H2 unquotierte Bezeichner klein anlegen (wie PostgreSQL, dafür steht es in
|
||||
* der URL), Liquibase sucht seine Verwaltungstabellen aber unter
|
||||
* `DATABASECHANGELOG`, findet nichts und legt sie an — woraufhin H2
|
||||
* „Table databasechangelog already exists" sagt. Der **erste** Start ging,
|
||||
* jeder weitere stürzte ab.
|
||||
*
|
||||
* Gefunden hat das erst der Server, nicht die Testsuite: Jeder Test bekommt
|
||||
* eine frische In-Memory-Datenbank, und auch von Hand gestartet wurde immer
|
||||
* gegen ein leeres Verzeichnis. Der Fall „starte noch einmal" kam schlicht nie
|
||||
* vor — bis der erste Deploy ihn zum Regelfall machte (D77-Nachtrag).
|
||||
*
|
||||
* Deshalb hier eine **Datei**-Datenbank und zwei Starts nacheinander. Der Test
|
||||
* kostet zwei Kontext-Starts; das ist er wert.
|
||||
*/
|
||||
class RestartTest {
|
||||
|
||||
@Test
|
||||
fun `startet auch gegen eine bestehende Datenbank`(@TempDir dir: Path) {
|
||||
repeat(2) { lauf ->
|
||||
val context = SpringApplicationBuilder(EditorBackendApplication::class.java)
|
||||
.run(
|
||||
// Überschrieben wird **nur das Verzeichnis**, nicht die
|
||||
// ganze JDBC-URL: Sonst prüfte der Test eine URL, die er
|
||||
// sich selbst ausgedacht hat, und die ausgelieferte bliebe
|
||||
// ungeprüft — genau daran ist der erste Anlauf gescheitert.
|
||||
//
|
||||
// Und als **Argument**, nicht als `properties(...)`:
|
||||
// Letztere sind Default-Properties mit der NIEDRIGSTEN
|
||||
// Priorität, die `application.yaml` überstimmt sie.
|
||||
"--werkbaum.data-dir=$dir",
|
||||
"--server.port=0",
|
||||
"--werkbaum.master-password.hash={noop}egal",
|
||||
)
|
||||
try {
|
||||
context.isRunning shouldBe true
|
||||
} finally {
|
||||
context.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import org.springframework.test.web.servlet.client.RestTestClient
|
||||
import java.util.Base64
|
||||
@@ -18,6 +19,7 @@ import java.util.Base64
|
||||
* UUID offen da, und das Zugriffsmodell (D76) wäre hinfällig — ohne dass es
|
||||
* jemandem auffiele.
|
||||
*/
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureRestTestClient
|
||||
@TestPropertySource(
|
||||
@@ -26,8 +28,8 @@ import java.util.Base64
|
||||
// Eigene Datenbank: Dieser Test braucht einen zweiten Spring-Kontext,
|
||||
// und zwei Kontexte auf derselben In-Memory-H2 stolpern uebereinander
|
||||
// (Liquibase legt DATABASECHANGELOG ein zweites Mal an).
|
||||
"spring.datasource.url=jdbc:h2:mem:editor-master-pw;MODE=PostgreSQL;" +
|
||||
"DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1",
|
||||
"spring.datasource.url=jdbc:h2:mem:editor-master-pw;" +
|
||||
"DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1",
|
||||
]
|
||||
)
|
||||
class MasterPasswordDefaultTest {
|
||||
|
||||
@@ -8,7 +8,9 @@ import io.cucumber.spring.CucumberContextConfiguration
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
|
||||
@ActiveProfiles("test")
|
||||
@CucumberContextConfiguration
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
// Seit Boot 4 stellt @SpringBootTest die Test-Client-Bean nicht mehr von selbst bereit
|
||||
|
||||
@@ -71,6 +71,22 @@ class DocumentStepDefinitions {
|
||||
lastResponse = listDocuments("test-geheim")
|
||||
}
|
||||
|
||||
@Wenn("ich die Info des Dienstes abrufe")
|
||||
fun `ich rufe die Info ab`() {
|
||||
lastResponse = client.get()
|
||||
.uri("/api/v1/info")
|
||||
.exchange()
|
||||
.returnResult(String::class.java)
|
||||
}
|
||||
|
||||
@Und("die Antwort nennt einen Namen und eine Version")
|
||||
fun `die Antwort nennt Name und Version`() {
|
||||
withClue("Antwort: ${body()}") {
|
||||
body() shouldContain "\"name\""
|
||||
body() shouldContain "\"version\""
|
||||
}
|
||||
}
|
||||
|
||||
@Wenn("ich alle Dokumente ohne Master-Passwort abrufe")
|
||||
fun `ich rufe alle Dokumente ohne Passwort ab`() {
|
||||
lastResponse = client.get()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Nur die Abweichungen fuer Tests. Bewusst `application-test.yaml` und nicht
|
||||
# `application.yaml`: Gleichnamig VERDECKT die Testdatei die Hauptkonfiguration
|
||||
# vollstaendig, und dann pruefen die Tests eine Konfiguration, die in
|
||||
# Produktion nie laeuft. Als Profil-Ueberlagerung laedt Spring erst die
|
||||
# Hauptdatei und legt diese darueber - Fehler wie die Liquibase-Schreibweise
|
||||
# (D77-Nachtrag) fallen so im Test auf statt beim zweiten Start am Server.
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:editor-test;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1
|
||||
|
||||
werkbaum:
|
||||
live-editing:
|
||||
# Kurz, damit die Behavior-Tests nicht auf die Produktionswerte warten.
|
||||
max-wait: 5s
|
||||
|
||||
master-password:
|
||||
# {noop} = Klartext. Nur im Test; in Produktion steht hier {bcrypt}$2a$...
|
||||
hash: "{noop}test-geheim"
|
||||
max-attempts: 3
|
||||
lockout: 2s
|
||||
@@ -1,27 +0,0 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:editor-test;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1
|
||||
username: sa
|
||||
password: ""
|
||||
driver-class-name: org.h2.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
open-in-view: false
|
||||
liquibase:
|
||||
change-log: classpath:db/changelog/db.changelog-master.sql
|
||||
|
||||
threads:
|
||||
virtual:
|
||||
enabled: true
|
||||
|
||||
werkbaum:
|
||||
live-editing:
|
||||
# Kurz, damit die Behavior-Tests nicht auf die Produktionswerte warten.
|
||||
max-wait: 5s
|
||||
|
||||
master-password:
|
||||
# {noop} = Klartext. Nur im Test; in Produktion steht hier {bcrypt}$2a$...
|
||||
hash: "{noop}test-geheim"
|
||||
max-attempts: 3
|
||||
lockout: 2s
|
||||
@@ -4,6 +4,15 @@ Funktionalität: Dokumente verwalten
|
||||
möchte ich Dokumente anlegen, abrufen, ändern und löschen können,
|
||||
damit das Backend die Grundlage für den Editor bildet.
|
||||
|
||||
Szenario: Der Dienst sagt, wer er ist
|
||||
Wenn ich die Info des Dienstes abrufe
|
||||
Dann erhalte ich den Status 200
|
||||
Und die Antwort nennt einen Namen und eine Version
|
||||
|
||||
Szenario: Die Info ist ohne Master-Passwort zu haben
|
||||
Wenn ich die Info des Dienstes abrufe
|
||||
Dann erhalte ich den Status 200
|
||||
|
||||
Szenario: Ein neues Dokument anlegen
|
||||
Wenn ich ein Dokument mit dem Titel "Notizen" und dem Inhalt "Hallo Welt" anlege
|
||||
Dann erhalte ich den Status 201
|
||||
|
||||
Reference in New Issue
Block a user