48f3336956
gates / gates (push) Successful in 23s
The notes a background run SAVES — last night's backup line, the last error, the proof result, the restore outcome — are written in the BOX's language at the moment they are written. A household that switches sees the previous run's note in the old language until the next run rewrites it: the operator's §16 option 1, stated rather than hidden. EndRestoreOp no longer receives a Hungarian literal from anywhere. The language switch is a globe. Two text links wrapped in the sidebar footer and asked the reader to recognise "Magyar"/"English" as links; a globe is the one symbol every web user already reads as "language", so nobody has to read Hungarian to escape Hungarian. It is <details>/<summary> — a menu with no script, drawn inline because the icon sprite lives only in layout.html and the visitor pages have their own shell. Those visitor pages get the same globe, and a visitor's choice stays theirs: a display-only felhom_lang cookie that langFor reads ONLY when there is no session. A signed-in household can never inherit a language a previous visitor picked in the same browser. POST /lang is CSRF-exempt for a narrow reason written at the exemption — its only achievable effect is the language of the page the victim's own browser shows them — and safeBackPath refuses //evil.example as well as https://, because "starts with /" alone is not the test. §16 taken: a successful claim carries the cookie into the household's setting. TWO PARITY EXCEPTIONS, MEASURED: 106 fixtures compared with a real diff — exactly two change shapes (the dashboard footer, the globe in the shells) and 5 byte-identical, which are the three pages that must not change. I INTRODUCED A DEADLOCK AND THE SUITE CAUGHT IT BY HANGING. UpdateOffboxStatus holds the settings write lock while running its callback; boxLang() wants the read lock; sync.RWMutex is not reentrant. On a real box an off-site run would have hung forever HOLDING the settings lock. Fixed by resolving the language before the callback, and guarded by a test that names the file and line in a second instead of hanging for 25 minutes. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
119 lines
5.5 KiB
Go
119 lines
5.5 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
)
|
|
|
|
// R-355, the sentence. „Ennek az alkalmazásnak nincs adatbázisa" is a claim ABOUT THE APP, and it was
|
|
// being inferred from a counter that has two different causes. On 2026-08-21 it was printed over a live
|
|
// 72-table PostgreSQL that the controller had dumped five minutes earlier.
|
|
//
|
|
// `SafetyDump` is the honest discriminator: writeSafetyDump returns a path only when a live database
|
|
// for this app was found AND successfully dumped.
|
|
|
|
func TestReconstituteOutcome_NoDatabaseOnlyWhenThereIsNone(t *testing.T) {
|
|
msg := reconstituteOutcomeMsg("opengist", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 3, DBsReplayed: 0, SafetyDump: "",
|
|
}, "hu")
|
|
if !strings.Contains(msg, "nincs adatbázisa") {
|
|
t.Errorf("an app with genuinely no database should still say so; got %q", msg)
|
|
}
|
|
}
|
|
|
|
// The one that matters: a database exists, none was replayed. Saying "this app has no database" here
|
|
// is false, and saying nothing at all would leave a silent loss under a success.
|
|
func TestReconstituteOutcome_DatabaseExistsButWasNotRestored(t *testing.T) {
|
|
msg := reconstituteOutcomeMsg("paperless-ngx", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 0, DBsReplayed: 0,
|
|
SafetyDump: "/mnt/x/db-dumps/pre-restore-20260822T060000Z-paperless-ngx-postgres.sql",
|
|
}, "hu")
|
|
if strings.Contains(msg, "nincs adatbázisa") {
|
|
t.Fatalf("FALSE CLAIM: told the customer the app has no database while its undo copy proves it does; got %q", msg)
|
|
}
|
|
for _, want := range []string{"VAN adatbázisa", "NEM állt vissza"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("the message must state that a database exists and did not come back; missing %q in %q", want, msg)
|
|
}
|
|
}
|
|
// The undo copy is the customer's way back, so it has to be named.
|
|
if !strings.Contains(msg, "pre-restore-20260822T060000Z-paperless-ngx-postgres.sql") {
|
|
t.Errorf("the message must name the undo copy; got %q", msg)
|
|
}
|
|
// It must never leak the directory — only the file name.
|
|
if strings.Contains(msg, "/mnt/x/") {
|
|
t.Errorf("the message leaked a filesystem path; got %q", msg)
|
|
}
|
|
}
|
|
|
|
func TestReconstituteOutcome_DatabaseRestoredIsUnchanged(t *testing.T) {
|
|
msg := reconstituteOutcomeMsg("romm", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 4, DBsReplayed: 1, SafetyDump: "/x/pre-restore-romm-mariadb.sql",
|
|
}, "hu")
|
|
if !strings.Contains(msg, "és az adatbázis visszaállítva") {
|
|
t.Errorf("the full case must keep its wording; got %q", msg)
|
|
}
|
|
if strings.Contains(msg, "FIGYELEM") {
|
|
t.Errorf("a complete restore must not carry a warning; got %q", msg)
|
|
}
|
|
}
|
|
|
|
// ── R-354: the volume leg has to reach the sentence ─────────────────────────────────────────────
|
|
|
|
// The 2026-08-21 case, as the customer saw it and as they must see it now.
|
|
func TestReconstituteOutcome_VolumesAreNamed(t *testing.T) {
|
|
msg := reconstituteOutcomeMsg("calibre-web", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 5, VolumesReplayed: 1, DBsReplayed: 0, SafetyDump: "",
|
|
}, "hu")
|
|
if !strings.Contains(msg, "5 fájl és 1 adatkötet visszaállítva") {
|
|
t.Errorf("the message must name the volume that came back; got %q", msg)
|
|
}
|
|
// The old sentence — five files and nothing else — must be gone.
|
|
if strings.Contains(msg, "5 fájl visszaállítva") {
|
|
t.Errorf("the pre-fix wording is still being produced; got %q", msg)
|
|
}
|
|
}
|
|
|
|
// A volume-only app: the whole dataset is the volume, and "0 fájl" alone said nothing about it.
|
|
func TestReconstituteOutcome_VolumeOnlyAppSaysWhatCameBack(t *testing.T) {
|
|
msg := reconstituteOutcomeMsg("privatebin", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 0, VolumesReplayed: 1, DBsReplayed: 0, SafetyDump: "",
|
|
}, "hu")
|
|
if !strings.Contains(msg, "0 fájl és 1 adatkötet visszaállítva") {
|
|
t.Errorf("a volume-only restore must state the volume; got %q", msg)
|
|
}
|
|
}
|
|
|
|
// Scenario C: a snapshot with no volume archives must produce the EXACT sentence it produced before,
|
|
// so the change cannot be read as "a volume was expected and did not arrive".
|
|
func TestReconstituteOutcome_NoVolumesWordingUnchanged(t *testing.T) {
|
|
noDB := reconstituteOutcomeMsg("opengist", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 3, VolumesReplayed: 0, DBsReplayed: 0, SafetyDump: "",
|
|
}, "hu")
|
|
if noDB != "A(z) opengist: 3 fájl visszaállítva — az alkalmazás újraindult. Ennek az alkalmazásnak nincs adatbázisa." {
|
|
t.Errorf("the no-volume, no-database wording changed; got %q", noDB)
|
|
}
|
|
withDB := reconstituteOutcomeMsg("romm", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 4, VolumesReplayed: 0, DBsReplayed: 1, SafetyDump: "/x/pre-restore-romm-mariadb.sql",
|
|
}, "hu")
|
|
if withDB != "A(z) romm: 4 fájl és az adatbázis visszaállítva — az alkalmazás újraindult." {
|
|
t.Errorf("the no-volume, with-database wording changed; got %q", withDB)
|
|
}
|
|
if strings.Contains(noDB, "adatkötet") || strings.Contains(withDB, "adatkötet") {
|
|
t.Error("a restore that replayed no volume must not mention volumes at all")
|
|
}
|
|
}
|
|
|
|
// All three legs at once.
|
|
func TestReconstituteOutcome_AllThreeLegs(t *testing.T) {
|
|
msg := reconstituteOutcomeMsg("paperless-ngx", backup.OffsiteReconstituteResult{
|
|
FilesPlaced: 12, VolumesReplayed: 3, DBsReplayed: 1, SafetyDump: "/x/pre-restore-p.sql",
|
|
}, "hu")
|
|
want := "A(z) paperless-ngx: 12 fájl és 3 adatkötet és az adatbázis visszaállítva — az alkalmazás újraindult."
|
|
if msg != want {
|
|
t.Errorf("got %q\nwant %q", msg, want)
|
|
}
|
|
}
|