package appbackup import ( "os" "path/filepath" "strings" "testing" ) // R-44 content-sniff tests. // // The dump that triggered this work (DIAG-immich-restore-2026-07-19) was 52MB, had a valid // PostgreSQL header and 60+ CREATE TABLE statements, and contained zero users and zero assets — // its entire bulk was immich's shipped geodata reference tables. Both of the signals the product // already had (file size, table count) called it healthy. These tests pin the one signal that // would have caught it, and the boundaries that keep it from crying wolf. func writeDump(t *testing.T, body string) string { t.Helper() p := filepath.Join(t.TempDir(), "d.sql") if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } const pgHead = `-- PostgreSQL database dump -- Dumped from database version 16.10 SET statement_timeout = 0; SET client_encoding = 'UTF8'; CREATE TABLE public.asset (id uuid NOT NULL); CREATE TABLE public."user" (id uuid NOT NULL, email text); ` // TestSniffFlagsEmptyAccountsTable is the 2026-07-19 shape: structurally perfect, no customer. func TestSniffFlagsEmptyAccountsTable(t *testing.T) { body := pgHead + "COPY public.\"user\" (id, email) FROM stdin;\n\\.\n" + "COPY public.asset (id) FROM stdin;\n\\.\n" v := ValidateDump(writeDump(t, body), DBTypePostgres) if !v.Valid { t.Fatalf("the dump is structurally valid; sniff must not change that: %s", v.Error) } if !v.UserTableFound { t.Fatal("the accounts table COPY block was not recognised") } if v.UserRows != 0 { t.Fatalf("UserRows = %d, want 0", v.UserRows) } if !v.LooksEmpty { t.Fatal("a valid dump with zero account rows MUST raise the warn signal — this is the whole point of R-44") } } // TestSniffQuietOnPopulatedDump — the common case must stay silent, or the warning becomes noise // and gets ignored precisely when it matters. func TestSniffQuietOnPopulatedDump(t *testing.T) { body := pgHead + "COPY public.\"user\" (id, email) FROM stdin;\n" + "a\tone@example.invalid\nb\ttwo@example.invalid\n\\.\n" v := ValidateDump(writeDump(t, body), DBTypePostgres) if v.UserRows != 2 { t.Fatalf("UserRows = %d, want 2", v.UserRows) } if v.LooksEmpty { t.Fatal("a dump with account rows must not be flagged") } } // TestSniffInconclusiveWithoutAccountsTable — plenty of legitimate apps have no users table. No // table, no claim: a false positive here would warn on every restore of such an app forever. func TestSniffInconclusiveWithoutAccountsTable(t *testing.T) { body := "-- PostgreSQL database dump\nCREATE TABLE public.thing (id int);\n" + "COPY public.thing (id) FROM stdin;\n\\.\n" + strings.Repeat("-- pad\n", 20) v := ValidateDump(writeDump(t, body), DBTypePostgres) if v.UserTableFound { t.Fatal("no accounts table exists — none must be reported") } if v.LooksEmpty { t.Fatal("an app without an accounts table must be INCONCLUSIVE, never flagged empty") } } // TestSniffIgnoresJoinAndAuditTables is the false-alarm guard that shaped the name list, and it is // written as the case that DISCRIMINATES: an app with NO accounts table but with `user_metadata` / // `album_user` / `user_audit` — all legitimately empty on a healthy box. Exact-matching leaves this // inconclusive (silent, correct). A substring match on "user" would treat a join table as the // accounts table, find zero rows, and shout "your backup looks empty" on every single restore of a // perfectly healthy app — which is how a warning signal becomes noise and then gets ignored. func TestSniffIgnoresJoinAndAuditTables(t *testing.T) { body := "-- PostgreSQL database dump\nCREATE TABLE public.album (id int);\n" + "COPY public.user_metadata (id) FROM stdin;\n\\.\n" + "COPY public.album_user (id) FROM stdin;\n\\.\n" + "COPY public.user_audit (id) FROM stdin;\n\\.\n" + "COPY public.album (id) FROM stdin;\n1\n\\.\n" v := ValidateDump(writeDump(t, body), DBTypePostgres) if v.UserTableFound { t.Fatal("a join/audit table must never be mistaken for the accounts table") } if v.LooksEmpty { t.Fatal("empty join/audit tables must not trigger the warning — this app has no accounts table at all") } } // TestSniffCountsOnlyTheAccountsTable pins the counting boundary separately: with a real accounts // table present, rows from neighbouring user-ish tables must not inflate it. func TestSniffCountsOnlyTheAccountsTable(t *testing.T) { body := pgHead + "COPY public.user_metadata (id) FROM stdin;\nm1\nm2\nm3\n\\.\n" + "COPY public.\"user\" (id, email) FROM stdin;\na\tone@example.invalid\n\\.\n" v := ValidateDump(writeDump(t, body), DBTypePostgres) if v.UserRows != 1 { t.Fatalf("only the real accounts table may be counted; UserRows = %d, want 1", v.UserRows) } } // TestSniffCountsWideRows — a row wider than the read buffer is skipped by the structural scan, but // it is still a row. Counting it wrong would flag a populated table as empty (immich asset rows are // genuinely long, which is what makes this reachable). func TestSniffCountsWideRows(t *testing.T) { wide := strings.Repeat("x", 300*1024) body := pgHead + "COPY public.\"user\" (id, email) FROM stdin;\n" + wide + "\n\\.\n" v := ValidateDump(writeDump(t, body), DBTypePostgres) if v.UserRows != 1 { t.Fatalf("a buffer-exceeding row must still count; UserRows = %d, want 1", v.UserRows) } if v.LooksEmpty { t.Fatal("a table whose single row is very wide must not sniff as empty") } } // TestSniffMariaDBInsertForm — mysqldump writes multi-row INSERTs, not COPY blocks. func TestSniffMariaDBInsertForm(t *testing.T) { head := "-- MariaDB dump 10.19\nCREATE TABLE `users` (id int);\n" + strings.Repeat("-- pad\n", 20) empty := ValidateDump(writeDump(t, head), DBTypeMariaDB) if empty.UserTableFound { t.Fatal("a CREATE TABLE alone is not an accounts-table row source") } full := ValidateDump(writeDump(t, head+"INSERT INTO `users` VALUES (1),(2);\n"), DBTypeMariaDB) if !full.UserTableFound || full.LooksEmpty { t.Fatalf("a populated mariadb dump must not be flagged: %+v", full) } }