package traffic import ( "database/sql" "os" "path/filepath" "testing" ) // openLegacyIndex builds the index exactly as the pre-reclamation Open did: a // plain-path DSN, pragmas via the pool, and auto_vacuum left at its default 0. func openLegacyIndex(t *testing.T, dir string) *sql.DB { t.Helper() if err := os.MkdirAll(filepath.Join(dir, "_index"), 0o755); err != nil { t.Fatal(err) } old, err := sql.Open("sqlite", filepath.Join(dir, "_index", "index.sqlite")) if err != nil { t.Fatal(err) } for _, p := range []string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000"} { if _, err := old.Exec(p); err != nil { t.Fatal(err) } } if _, err := old.Exec(indexSchema); err != nil { t.Fatal(err) } return old } // TestUpgradeFromOldInstall guards the upgrade path. Open now names the database // through a file: URI so per-connection pragmas can ride in the DSN, and a // driver that did not treat that as a URI would quietly open a file literally // named "file:/…" — an empty index, with every recorded exchange apparently // gone. The assertions below are what prove that does not happen. func TestUpgradeFromOldInstall(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "_index", "index.sqlite") old := openLegacyIndex(t, dir) if _, err := old.Exec(ftsSchema); err != nil { t.Fatal(err) } // 三条历史流量,含一条 legacy path<>'' 的行 for i, row := range [][]any{ {"1700000000-0001", "old.example.com", ""}, {"1700000000-0002", "old.example.com", ""}, {"1700000000-0003", "legacy.example.com", "legacy.example.com/GET/x"}, } { if _, err := old.Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path) VALUES(?,?,?,'GET','/x','http://x/x',200,'text/html',0,9,?)`, row[0], 1700000000+i, row[1], row[2]); err != nil { t.Fatal(err) } if _, err := old.Exec(`INSERT INTO exchange_bodies(id,req_head,req_body,resp_head,resp_body) VALUES(?,'GET /x','','HTTP 200','老数据正文')`, row[0]); err != nil { t.Fatal(err) } if _, err := old.Exec(`INSERT INTO ex_fts(rowid,content) VALUES(?,?)`, i+1, "老数据正文 secret-token"); err != nil { t.Fatal(err) } } if err := old.Close(); err != nil { t.Fatal(err) } stat, err := os.Stat(path) if err != nil { t.Fatal(err) } // ---- 新版本接管 tr, err := Open(dir, "127.0.0.1:0") if err != nil { t.Fatalf("新版本无法打开旧库: %v", err) } defer tr.Close() // 1. 必须是同一个文件,不能悄悄开了个新空库 if st2, err := os.Stat(path); err != nil || st2.Size() == 0 { t.Fatalf("原索引文件异常: size=%v err=%v", st2, err) } if entries, _ := os.ReadDir(filepath.Join(dir, "_index")); len(entries) > 3 { for _, e := range entries { t.Logf("_index 下: %s", e.Name()) } t.Fatal("_index 下出现了预期外的文件,DSN 可能指向了别的库") } t.Logf("旧库 %d 字节,新版本接管后仍是同一文件", stat.Size()) // 2. 历史数据全部可见 n, err := tr.Count() if err != nil || n != 3 { t.Fatalf("Count=(%d,%v),应为 (3,nil) —— 历史流量丢失", n, err) } // 3. 历史全文索引仍可搜 if tr.fts { rows, err := tr.query("old.example.com", "", "secret-token", 0, 10) if err != nil { t.Fatalf("历史全文搜索失败: %v", err) } if len(rows) != 2 { t.Fatalf("历史全文搜索命中 %d 条,应为 2", len(rows)) } } // 4. 历史正文仍可读 if _, resp, err := tr.Get("1700000000-0001"); err != nil { t.Fatalf("读取历史正文失败: %v", err) } else if resp == "" { t.Fatal("历史响应为空") } // 5. 旧库不会被误判为已启用增量回收 if tr.incrementalVacuum { t.Fatal("旧库被误判为已启用增量回收") } // 6. 删除仍然正常工作,且回收流程在旧库上能收敛 deleted, err := tr.DeleteHostsExact([]string{"old.example.com"}) if err != nil || deleted != 2 { t.Fatalf("DeleteHostsExact=(%d,%v),应为 (2,nil)", deleted, err) } tr.reaping.Wait() if n, err := tr.Count(); err != nil || n != 1 { t.Fatalf("删除后 Count=(%d,%v),应为 (1,nil)", n, err) } // 7. legacy path<>'' 的行没被牵连 var legacyPath string if err := tr.DB().QueryRow(`SELECT path FROM exchanges`).Scan(&legacyPath); err != nil { t.Fatal(err) } if legacyPath == "" { t.Fatal("legacy 行的 path 被清空了") } } // TestDowngradeToOldBinary covers a rollback: a database created with // auto_vacuum=incremental must stay readable and writable by a build that knows // nothing about it. auto_vacuum only changes where SQLite tracks free pages, so // the old binary simply goes back to never returning them. func TestDowngradeToOldBinary(t *testing.T) { dir := t.TempDir() tr, err := Open(dir, "127.0.0.1:0") if err != nil { t.Fatal(err) } if !tr.incrementalVacuum { t.Fatal("新库应启用增量回收") } bulkRecord(tr, "keep.example.com", 5, 100*1024) if err := tr.Close(); err != nil { t.Fatal(err) } old := openLegacyIndex(t, dir) // 旧版本二进制接管 defer old.Close() var n int if err := old.QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&n); err != nil || n != 5 { t.Fatalf("旧版本读到 (%d,%v),应为 (5,nil)", n, err) } if _, err := old.Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path) VALUES('x',1,'new.example.com','GET','/x','http://x/x',200,'',0,0,'')`); err != nil { t.Fatalf("旧版本写入失败: %v", err) } if _, err := old.Exec(`DELETE FROM exchanges WHERE host='keep.example.com'`); err != nil { t.Fatalf("旧版本删除失败: %v", err) } }