package services // Pure-function tests for the Backup Mode runtime (t-paliad-246 / m/paliad#77). // // Live DB behaviour (the actual org dump end-to-end) needs a Postgres; // it would live in backup_service_live_test.go under TEST_DATABASE_URL. // This file covers the bits that don't need a database: // // - orgSheetQueries registry shape: no duplicates, no excluded // paliadin sheets, predictable prefix split between entity and ref. // - LocalDiskStore Put / Get / Delete round-trip, key validation, // URI traversal rejection. import ( "bytes" "context" "io" "os" "path/filepath" "strings" "testing" ) // --------------------------------------------------------------------------- // orgSheetQueries registry // --------------------------------------------------------------------------- func TestOrgSheetQueries_NoDuplicates(t *testing.T) { seen := map[string]bool{} for _, sq := range orgSheetQueries() { if seen[sq.SheetName] { t.Fatalf("duplicate sheet name in orgSheetQueries: %q", sq.SheetName) } seen[sq.SheetName] = true } } func TestOrgSheetQueries_ExcludesPaliadinTables(t *testing.T) { // m's t-paliad-214 Q5 decision + this design's §11 Q3 default: // paliadin_turns and paliadin_aichat_conversation must be ABSENT // from the registry (structural exclusion, not just column-drop). for _, sq := range orgSheetQueries() { name := sq.SheetName if strings.Contains(name, "paliadin") { t.Fatalf("orgSheetQueries leaked paliadin sheet: %q (m's Q3 mandates structural exclusion)", name) } // Belt-and-braces: SQL bodies should not reference the tables // either (no UNION joins, no subqueries pulling them in). if strings.Contains(sq.SQL, "paliadin_turns") || strings.Contains(sq.SQL, "paliadin_aichat_conversation") { t.Fatalf("orgSheetQueries[%q] SQL references a paliadin table: %s", name, sq.SQL) } } } func TestOrgSheetQueries_RefSheetsPrefixed(t *testing.T) { // Every sheet whose data is read-only reference material is // expected to use the `ref__` prefix. The writer's downstream // consumers rely on this convention to group reference data // visually in the workbook. for _, sq := range orgSheetQueries() { if !strings.HasPrefix(sq.SheetName, "ref__") { continue } // Reference sheets shouldn't carry per-row WHERE clauses (they // dump the whole reference table for portability). if strings.Contains(strings.ToUpper(sq.SQL), "WHERE") { t.Fatalf("orgSheetQueries[%q] is ref__ but has a WHERE clause; reference sheets dump the whole table", sq.SheetName) } } } func TestOrgSheetQueries_OrderByForDeterminism(t *testing.T) { // Every sheet must specify an ORDER BY so the byte-deterministic // contract from t-paliad-214 §3 holds across runs. for _, sq := range orgSheetQueries() { if !strings.Contains(strings.ToUpper(sq.SQL), "ORDER BY") { t.Fatalf("orgSheetQueries[%q] missing ORDER BY (determinism contract): %s", sq.SheetName, sq.SQL) } } } // --------------------------------------------------------------------------- // LocalDiskStore round-trip // --------------------------------------------------------------------------- func TestLocalDiskStore_RoundTrip(t *testing.T) { dir := t.TempDir() store, err := NewLocalDiskStore(dir) if err != nil { t.Fatalf("NewLocalDiskStore: %v", err) } ctx := context.Background() want := []byte("hello backup\n") uri, err := store.Put(ctx, "test.zip", want) if err != nil { t.Fatalf("Put: %v", err) } if !strings.HasPrefix(uri, "file://") { t.Fatalf("expected file:// uri, got %q", uri) } rc, size, err := store.Get(ctx, uri) if err != nil { t.Fatalf("Get: %v", err) } defer rc.Close() if size != int64(len(want)) { t.Fatalf("Get size = %d, want %d", size, len(want)) } got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } if !bytes.Equal(got, want) { t.Fatalf("Get body = %q, want %q", got, want) } if err := store.Delete(ctx, uri); err != nil { t.Fatalf("Delete: %v", err) } // File should be gone; Get returns an error. if _, _, err := store.Get(ctx, uri); err == nil { t.Fatalf("Get after Delete should fail") } // Delete is idempotent. if err := store.Delete(ctx, uri); err != nil { t.Fatalf("idempotent Delete: %v", err) } } func TestLocalDiskStore_RejectsBadKeys(t *testing.T) { dir := t.TempDir() store, err := NewLocalDiskStore(dir) if err != nil { t.Fatalf("NewLocalDiskStore: %v", err) } ctx := context.Background() cases := []string{ "", "sub/dir/file.zip", "..\\evil.zip", "../escape.zip", "/abs/path.zip", } for _, k := range cases { if _, err := store.Put(ctx, k, []byte("x")); err == nil { t.Fatalf("Put with bad key %q should fail", k) } } } func TestLocalDiskStore_RejectsURIOutsideDir(t *testing.T) { dir := t.TempDir() store, err := NewLocalDiskStore(dir) if err != nil { t.Fatalf("NewLocalDiskStore: %v", err) } ctx := context.Background() // A file:// URI pointing outside the store dir must be rejected // by both Get and Delete (defense in depth against a corrupted // catalog row). outside := "file://" + filepath.Join(filepath.Dir(dir), "elsewhere.zip") if _, _, err := store.Get(ctx, outside); err == nil { t.Fatalf("Get outside store dir should fail") } if err := store.Delete(ctx, outside); err == nil { t.Fatalf("Delete outside store dir should fail") } // Wrong scheme is also rejected. if _, _, err := store.Get(ctx, "https://example.com/foo.zip"); err == nil { t.Fatalf("Get with non-file:// scheme should fail") } } func TestLocalDiskStore_CreatesDir(t *testing.T) { // A non-existent parent gets created at construction; mode 0700. base := t.TempDir() target := filepath.Join(base, "nested", "exports") store, err := NewLocalDiskStore(target) if err != nil { t.Fatalf("NewLocalDiskStore(non-existent): %v", err) } info, err := os.Stat(target) if err != nil { t.Fatalf("expected store dir to exist: %v", err) } if !info.IsDir() { t.Fatalf("expected directory, got file") } // Smoke-write to confirm the dir is actually usable. if _, err := store.Put(context.Background(), "ok.zip", []byte{}); err != nil { t.Fatalf("Put into fresh dir: %v", err) } }