package cli import ( "bytes" "os" "path/filepath" "strings" "testing" "time" agecrypt "filippo.io/age" "gitea.mynodes.xyz/ilker/llmgw/internal/config" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) func TestABackupIsRestoredWhereItSays(t *testing.T) { tmp := t.TempDir() etc := filepath.Join(tmp, "etc") _ = os.MkdirAll(filepath.Join(etc, "secrets"), 0o750) dbPath := filepath.Join(tmp, "llmgw.db") _ = os.WriteFile(filepath.Join(etc, "server.yaml"), []byte("auth:\n enabled: true\n store: "+dbPath+"\n"), 0o640) _ = os.WriteFile(filepath.Join(etc, "secrets", "admin-token"), []byte(strings.Repeat("t", 40)), 0o600) db, err := store.Open(t.Context(), dbPath) if err != nil { t.Fatal(err) } if _, err := db.CreateOrg(t.Context(), "acme"); err != nil { t.Fatal(err) } _ = db.Close() id, _ := agecrypt.GenerateX25519Identity() to, _ := config.ParseBackupRecipients([]string{id.Recipient().String()}) archive, err := writeBackup(t.Context(), dbPath, etc, filepath.Join(tmp, "backups"), to) if err != nil { t.Fatal(err) } key := filepath.Join(tmp, "key.txt") _ = os.WriteFile(key, []byte(id.String()+"\n"), 0o600) // Onto another machine, as it were: an empty place for both. newEtc, newDB := filepath.Join(tmp, "new-etc"), filepath.Join(tmp, "new", "llmgw.db") var out bytes.Buffer rs := restore{archive: archive, identities: []string{key}, etcDir: newEtc, storeAddr: newDB, dryRun: true, out: &out, now: time.Now} if err := rs.run(t.Context()); err != nil { t.Fatal(err) } if _, err := os.Stat(newEtc); err == nil || !strings.Contains(out.String(), "dry run") { t.Fatalf("a dry run changed something:\n%s", out.String()) } out.Reset() rs.dryRun = false if err := rs.run(t.Context()); err != nil { t.Fatalf("%v\n%s", err, out.String()) } if b, _ := os.ReadFile(filepath.Join(newEtc, "secrets", "admin-token")); len(b) != 40 { t.Error("the configuration did not come back") } if fi, err := os.Stat(filepath.Join(newEtc, "secrets", "admin-token")); err != nil || fi.Mode().Perm() != 0o600 { t.Errorf("modes: %v %v", fi.Mode(), err) } db, err = store.Open(t.Context(), newDB) if err != nil { t.Fatal(err) } defer db.Close() if orgs, _ := db.AllOrgs(t.Context()); len(orgs) != 1 || orgs[0].Name != "acme" { t.Errorf("the store did not come back: %+v", orgs) } // Again over what is there: the current one is kept aside, not lost. out.Reset() if err := rs.run(t.Context()); err != nil { t.Fatal(err) } matches, _ := filepath.Glob(newEtc + ".before-restore-*") if len(matches) != 1 { t.Errorf("the configuration replaced was not kept: %v", matches) } // Without the key it is said, not guessed. rs.identities = nil if err := rs.run(t.Context()); err == nil || !strings.Contains(err.Error(), "--identity") { t.Errorf("%v", err) } } func TestANewerArchiveIsRefused(t *testing.T) { if !newer("v0.12.0", "v0.11.1-106-gabc") || newer("v0.11.1", "v0.11.1-5-g1") || newer("dev", "v1.0.0") { t.Error("version comparison") } } func TestAnArchiveIsCopiedAndTheOldestPruned(t *testing.T) { tmp := t.TempDir() dest := filepath.Join(tmp, "share") for i := range 4 { p := filepath.Join(tmp, "llmgw-backup-h-2026100"+string(rune('1'+i))+"T000000Z.tar.gz.age") _ = os.WriteFile(p, []byte("x"), 0o600) _ = os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Hour), time.Now().Add(time.Duration(i)*time.Hour)) if err := copyToDir(p, dest, 2); err != nil { t.Fatal(err) } _ = os.Chtimes(filepath.Join(dest, filepath.Base(p)), time.Now().Add(time.Duration(i)*time.Hour), time.Now().Add(time.Duration(i)*time.Hour)) } left, _ := filepath.Glob(filepath.Join(dest, "llmgw-backup-*")) if len(left) != 2 { t.Errorf("kept %v", left) } c := config.Default() c.BackupCopyTo = []string{"/mnt/x"} if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "backup_recipients") { t.Errorf("an unencrypted copy off the machine was allowed: %v", err) } }