Files
ARTEX/traffic/upgrade_test.go
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

165 lines
5.7 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)
}
// three historical flows, including one row with 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','legacy body text')`, row[0]); err != nil {
t.Fatal(err)
}
if _, err := old.Exec(`INSERT INTO ex_fts(rowid,content) VALUES(?,?)`, i+1, "legacy body text 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)
}
// ---- new version takes over
tr, err := Open(dir, "127.0.0.1:0")
if err != nil {
t.Fatalf("new version cannot open the old database: %v", err)
}
defer tr.Close()
// 1. must be the same file — no silently opening a new empty database
if st2, err := os.Stat(path); err != nil || st2.Size() == 0 {
t.Fatalf("original index file abnormal: size=%v err=%v", st2, err)
}
if entries, _ := os.ReadDir(filepath.Join(dir, "_index")); len(entries) > 3 {
for _, e := range entries {
t.Logf("under _index: %s", e.Name())
}
t.Fatal("unexpected files under _index; the DSN may point to a different database")
}
t.Logf("old database %d bytes, still the same file after the new version takes over", stat.Size())
// 2. all historical data is visible
n, err := tr.Count()
if err != nil || n != 3 {
t.Fatalf("Count=(%d,%v), want (3,nil) — historical traffic lost", n, err)
}
// 3. historical full-text index is still searchable
if tr.fts {
rows, err := tr.query("old.example.com", "", "secret-token", 0, 10)
if err != nil {
t.Fatalf("historical full-text search failed: %v", err)
}
if len(rows) != 2 {
t.Fatalf("historical full-text search hit %d rows, want 2", len(rows))
}
}
// 4. historical bodies are still readable
if _, resp, err := tr.Get("1700000000-0001"); err != nil {
t.Fatalf("failed to read historical body: %v", err)
} else if resp == "" {
t.Fatal("historical response is empty")
}
// 5. the old database must not be misdetected as incremental-vacuum-enabled
if tr.incrementalVacuum {
t.Fatal("old database misdetected as incremental-vacuum-enabled")
}
// 6. deletion still works, and reclamation converges on the old database
deleted, err := tr.DeleteHostsExact([]string{"old.example.com"})
if err != nil || deleted != 2 {
t.Fatalf("DeleteHostsExact=(%d,%v), want (2,nil)", deleted, err)
}
tr.reaping.Wait()
if n, err := tr.Count(); err != nil || n != 1 {
t.Fatalf("after delete Count=(%d,%v), want (1,nil)", n, err)
}
// 7. the legacy path<>'' row is not affected
var legacyPath string
if err := tr.DB().QueryRow(`SELECT path FROM exchanges`).Scan(&legacyPath); err != nil {
t.Fatal(err)
}
if legacyPath == "" {
t.Fatal("legacy row's path was cleared")
}
}
// 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("new database should enable incremental vacuum")
}
bulkRecord(tr, "keep.example.com", 5, 100*1024)
if err := tr.Close(); err != nil {
t.Fatal(err)
}
old := openLegacyIndex(t, dir) // old binary takes over
defer old.Close()
var n int
if err := old.QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&n); err != nil || n != 5 {
t.Fatalf("old version read (%d,%v), want (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("old version write failed: %v", err)
}
if _, err := old.Exec(`DELETE FROM exchanges WHERE host='keep.example.com'`); err != nil {
t.Fatalf("old version delete failed: %v", err)
}
}