package backup import ( "context" "io" "log" "os" "path/filepath" "reflect" "strings" "testing" "time" ) // R-1106 (`07` §7.4, operator decision 1A): a restored database comes back with the password it was saved with. const r1106Compose = `services: app: image: ghcr.io/x/app:1 environment: - DATABASE_URL=postgresql://app:${DB_PASSWORD}@app-db:5432/app app-db: image: postgres:18-alpine environment: - POSTGRES_USER=app - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - app_db_data:/var/lib/postgresql - ./init:/docker-entrypoint-initdb.d maria: image: mariadb:11.4 environment: MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-x}" MARIADB_PASSWORD: literal-not-a-variable MARIADB_DATABASE: app volumes: - type: volume source: maria_data target: /var/lib/mysql cache: image: redis:7-alpine environment: - REDIS_PASSWORD=${CACHE_PASSWORD} ` // r1106OneDB is one Postgres fed by DB_PASSWORD — the Rallly shape. const r1106OneDB = `services: app: image: ghcr.io/x/app:1 app-db: image: postgres:18-alpine environment: - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - app_db_data:/var/lib/postgresql ` func TestR1106_DBCredentialVarsAndVolumes(t *testing.T) { p := filepath.Join(t.TempDir(), "docker-compose.yml") mustWrite(t, p, r1106Compose) vars, err := DBCredentialVars(p) if err != nil { t.Fatal(err) } // list form, map form with a default; the literal yields nothing; redis is not a database. if want := []string{"DB_PASSWORD", "DB_ROOT_PASSWORD"}; !reflect.DeepEqual(vars, want) { t.Errorf("DBCredentialVars = %v, want %v", vars, want) } vols, err := DBDataVolumesFor(p) if err != nil { t.Fatal(err) } if want := []string{"app_db_data", "maria_data"}; !reflect.DeepEqual(vols, want) { t.Errorf("DBDataVolumes = %v, want %v (binds are not volumes)", vols, want) } } func TestR1106_PlanDBCredentials(t *testing.T) { data := time.Date(2026, 10, 16, 18, 0, 0, 0, time.UTC) before, after := data.Add(-24*time.Hour), data.Add(time.Hour) names := []string{"DB_PASSWORD"} cases := []struct { name string unit map[string]string live map[string]string installed time.Time dataAt time.Time wantErr bool wantUnit string }{ {"the unit's value wins over a reinstall's", map[string]string{"DB_PASSWORD": "old"}, map[string]string{"DB_PASSWORD": "new"}, after, data, false, "old"}, {"unit lacks it, install OLDER than the data → kept", nil, map[string]string{"DB_PASSWORD": "same"}, before, data, false, ""}, {"unit lacks it, install NEWER than the data → refused", nil, map[string]string{"DB_PASSWORD": "new"}, after, data, true, ""}, {"unit lacks it, install time unknown → refused", nil, map[string]string{"DB_PASSWORD": "new"}, time.Time{}, data, true, ""}, {"unit lacks it, data time unknown → refused", nil, map[string]string{"DB_PASSWORD": "new"}, before, time.Time{}, true, ""}, {"nowhere at all → refused", nil, nil, before, data, true, ""}, } for _, c := range cases { p, err := planDBCredentials(names, c.unit, c.live, c.installed, c.dataAt) if (err != nil) != c.wantErr { t.Errorf("%s: err = %v, wantErr %v", c.name, err, c.wantErr) continue } if p.FromUnit["DB_PASSWORD"] != c.wantUnit { t.Errorf("%s: FromUnit = %q, want %q", c.name, p.FromUnit["DB_PASSWORD"], c.wantUnit) } } } // r1106UnitRestoreFixture: a unit whose compose has a database fed by DB_PASSWORD, a live install deployed AFTER the // unit's data. unitPW "" = the unit carries no DB_PASSWORD. func r1106UnitRestoreFixture(t *testing.T, unitPW string, guest map[string]string) (*Manager, *fakeRecoveryProvider, *[]string) { t.Helper() tmp := t.TempDir() drive := filepath.Join(tmp, "drive") composeDir := RecoveryUnitComposePath(drive, "app") mustWrite(t, filepath.Join(composeDir, "docker-compose.yml"), r1106OneDB) env := "deployed: true\nenv:\n SUBDOMAIN: trips\n" portable := []string{} if unitPW != "" { env += " DB_PASSWORD: " + unitPW + "\n" portable = []string{"DB_PASSWORD"} } mustWrite(t, filepath.Join(composeDir, "app.yaml"), env) man := &RecoveryManifest{SchemaVersion: 2, AppName: "app", ControllerVer: "v", SecretEnvVars: []string{"DB_PASSWORD"}, PortableSecretEnvVars: portable, CreatedAt: "2026-10-16T18:00:00Z", DumpsAt: "2026-10-16T18:00:00Z"} if err := writeManifest(RecoveryUnitManifestPath(drive, "app"), man); err != nil { t.Fatal(err) } live := filepath.Join(tmp, "live") mustWrite(t, filepath.Join(live, "docker-compose.yml"), r1106OneDB) mustWrite(t, filepath.Join(live, "app.yaml"), "deployed: true\ndeployed_at: \"2026-10-16T19:00:00Z\"\nenv: {}\n") fake := &fakeRecoveryProvider{hdd: drive, running: true, secrets: guest, info: RecoveryInfo{StackDir: live}} m := &Manager{logger: log.New(io.Discard, "", 0), systemDataPath: filepath.Join(drive, "..", "sys"), stackProvider: fake} var gen []string m.generateSecret = func(_, envVar string) (string, bool) { gen = append(gen, envVar); return "GENERATED", true } return m, fake, &gen } // The on-box restore after a reinstall: the backup's password is the one the app gets. func TestR1106_UnitRestoreUsesTheBackupsDBPassword(t *testing.T) { m, fake, gen := r1106UnitRestoreFixture(t, "old-pw", map[string]string{"DB_PASSWORD": "new-pw-from-reinstall"}) if _, err := m.RestoreFromRecoveryUnit("app"); err != nil { t.Fatalf("restore: %v", err) } if fake.gotEnv["DB_PASSWORD"] != "old-pw" { t.Errorf("DB_PASSWORD = %q, want the backup's value", fake.gotEnv["DB_PASSWORD"]) } if len(*gen) != 0 { t.Errorf("a database password was generated: %v", *gen) } } // A unit without the database password, restored over a NEWER install: refused before anything changes, and never // a generated password (O4 used to make one — the R-127 (b) trap). func TestR1106_UnitRestoreNeverGeneratesADBPassword(t *testing.T) { for name, guest := range map[string]map[string]string{ "no value anywhere": nil, "only the reinstall's new value": {"DB_PASSWORD": "new-pw-from-reinstall"}, } { m, fake, gen := r1106UnitRestoreFixture(t, "", guest) _, err := m.RestoreFromRecoveryUnit("app") if err == nil || !strings.Contains(err.Error(), "adatbázis jelszavát") { t.Errorf("%s: want the refusal sentence, got %v", name, err) } if fake.gotEnv != nil || fake.stopped { t.Errorf("%s: the current install was touched (recreate %v, stopped %v)", name, fake.gotEnv != nil, fake.stopped) } if len(*gen) != 0 { t.Errorf("%s: a database password was generated: %v", name, *gen) } } } // credProvider wraps the reconstitute fixture's provider with the live install's secrets, its app.yaml (for the // install time) and the narrow writer, recording WHEN the writer ran relative to the other calls. type credProvider struct { *recordingProvider live map[string]string stackDir string written map[string]string } func (p *credProvider) RecoverStackSecrets(_ string, names []string) map[string]string { out := map[string]string{} for _, n := range names { if v, ok := p.live[n]; ok { out[n] = v } } return out } func (p *credProvider) GetStackRecoveryInfo(string) (RecoveryInfo, bool) { return RecoveryInfo{StackDir: p.stackDir}, true } func (p *credProvider) SetStackSecretValues(_ string, values map[string]string) error { p.written = values p.calls = append(p.calls, "setsecrets") return nil } // r1106ReconFixture: the reconstitute fixture with a database fed by DB_PASSWORD, a reinstall NEWER than the // snapshot, the snapshot's unit carrying unitPW (or not), and the DB volume archive in the snapshot when withVolume. func r1106ReconFixture(t *testing.T, unitPW string, withVolume bool) (*Manager, *credProvider) { t.Helper() m, prov, _ := reconFixture(t, "20261016T180000Z", "2026-10-16T18:00:00Z", pgDump(1)) compose := `services: immich-server: image: ghcr.io/immich-app/immich-server:v3.0.3 immich-postgres: image: ghcr.io/immich-app/postgres:16-vectorchord0.4.3-pgvectors0.2.0 environment: - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - immich_postgres_data:/var/lib/postgresql/data ` mustWrite(t, prov.composePath, compose) stackDir := filepath.Dir(prov.composePath) mustWrite(t, filepath.Join(stackDir, "app.yaml"), "deployed: true\ndeployed_at: \"2026-10-16T19:00:00Z\"\nenv: {}\n") cp := &credProvider{recordingProvider: prov, live: map[string]string{"DB_PASSWORD": "new-pw-from-reinstall"}, stackDir: stackDir} m.SetStackProvider(cp) scratch, _, err := m.offboxRestoreScratchDir("immich") if err != nil { t.Fatal(err) } unit := findScratchUnitDir(scratch, "immich") env := "deployed: true\nenv:\n SUBDOMAIN: photos\n" portable := []string{} if unitPW != "" { env += " DB_PASSWORD: " + unitPW + "\n" portable = []string{"DB_PASSWORD"} } mustWrite(t, filepath.Join(UnitComposeDir(unit), "app.yaml"), env) man := &RecoveryManifest{SchemaVersion: 2, AppName: "immich", OffsiteRunID: "20261016T180000Z", DumpsAt: "2026-10-16T18:00:00Z", SecretEnvVars: []string{"DB_PASSWORD"}, PortableSecretEnvVars: portable} if err := writeManifest(filepath.Join(unit, "manifest.json"), man); err != nil { t.Fatal(err) } if withVolume { seedScratchVolumes(t, m, "immich", "immich_immich_postgres_data.tar") } m.volumeReplayFrom = func(_, dir string) (int, error) { cp.calls = append(cp.calls, "volumes") entries, _ := os.ReadDir(dir) return len(entries), nil } return m, cp } // The drill's shape (Rallly, Grimmory, 2026-10-16): reinstall, then an off-site full restore that replays the DB // volume. The backup's password must be in the app's settings BEFORE the database starts. func TestR1106_OffsiteRestoreWritesTheBackupsDBPasswordBeforeTheDBStarts(t *testing.T) { m, cp := r1106ReconFixture(t, "old-pw", true) if _, err := m.ReconstituteFromOffsite(context.Background(), "immich", false); err != nil { t.Fatalf("ReconstituteFromOffsite: %v", err) } if cp.written["DB_PASSWORD"] != "old-pw" { t.Fatalf("the backup's password was not written (got %v)", cp.written) } set, db := -1, -1 for i, c := range cp.calls { if c == "setsecrets" && set < 0 { set = i } if strings.HasPrefix(c, "startsvc:") && db < 0 { db = i } } if set < 0 || db < 0 || set > db { t.Errorf("the password must be written before the database service starts; calls = %v", cp.calls) } } // The unit carries no password and the install is newer than the snapshot: refused, nothing stopped or written. func TestR1106_OffsiteRestoreRefusesWithoutTheDBPassword(t *testing.T) { m, cp := r1106ReconFixture(t, "", true) _, err := m.ReconstituteFromOffsite(context.Background(), "immich", false) if err == nil || !strings.Contains(err.Error(), "adatbázis jelszavát") { t.Fatalf("want the refusal sentence, got %v", err) } for _, c := range cp.calls { if c == "stop" || c == "setsecrets" || c == "volumes" { t.Errorf("the current install was touched: calls = %v", cp.calls) break } } } // A snapshot that replays only a logical dump (no DB volume) must leave the current password alone: the dump goes // into the current data directory, which only the current password opens. func TestR1106_DumpOnlySnapshotKeepsTheCurrentPassword(t *testing.T) { m, cp := r1106ReconFixture(t, "old-pw", false) if _, err := m.ReconstituteFromOffsite(context.Background(), "immich", false); err != nil { t.Fatalf("ReconstituteFromOffsite: %v", err) } if cp.written != nil { t.Errorf("a dump-only restore rewrote the password: %v", cp.written) } }