Files
artex/traffic/upgrade_test.go
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

165 lines
5.5 KiB
Go

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)
}
}