// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "database/sql" "os" "path/filepath" "reflect" "strings" "sync" "testing" "time" sqlite3 "modernc.org/sqlite/lib" ) func TestOpenDatabaseCreatesFile(t *testing.T) { t.Parallel() path := testDBPath(t) if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("file should not exist yet: %v", err) } log, buf := newTestLogger(LevelTrace) db, err := OpenDatabase(context.Background(), path, log) if err != nil { t.Fatalf("OpenDatabase: %v", err) } defer db.Close() if fi, err := os.Stat(path); err != nil || fi.Size() == 0 { t.Fatalf("database file not created: %v", err) } for _, want := range []string{ `level=TRACE msg="function entry" func=OpenDatabase`, `level=DEBUG msg="database schema initialized"`, `level=DEBUG msg="database opened"`, `level=TRACE msg="function exit" func=OpenDatabase`, } { if !strings.Contains(buf.String(), want) { t.Errorf("missing log %q", want) } } } func TestOpenDatabaseOddPaths(t *testing.T) { t.Parallel() dir := t.TempDir() for _, name := range []string{ "with space.db", "q?mark.db", "hash#.db", "pct%20.db", "pct%.db", "amp&eq=.db", "ünï©ødé.db", ":memory:", "semi;colon.db", "quote'\".db", "back\\slash.db", } { path := filepath.Join(dir, name) db, err := OpenDatabase(context.Background(), path, traceDiscard()) if err != nil { t.Errorf("%q: %v", name, err) continue } mustExec(t, db, `INSERT INTO tokens VALUES ('t', '', 1, 'x', NULL)`) db.Close() // The file exists under the exact literal name if _, err := os.Stat(path); err != nil { t.Errorf("%q: file not created literally: %v", name, err) } } } func TestOpenDatabaseRelativePath(t *testing.T) { t.Parallel() dsn, err := sqliteDSN("rel/x y.db") if err != nil { t.Fatal(err) } wd, _ := os.Getwd() want := "file://" + escapeURIPath(filepath.Join(wd, "rel/x y.db")) + "?" + dsnParams if dsn != want { t.Fatalf("dsn = %q, want %q", dsn, want) } if !strings.Contains(dsn, "x%20y.db") { t.Fatalf("space not escaped: %q", dsn) } } func TestEscapeURIPath(t *testing.T) { t.Parallel() got := escapeURIPath("/a b/?#%&=~._-AZaz09/ü") want := "/a%20b/%3F%23%25%26%3D~._-AZaz09/%C3%BC" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestOpenDatabaseFailure(t *testing.T) { t.Parallel() // A directory cannot be opened as a database if _, err := OpenDatabase(context.Background(), t.TempDir(), traceDiscard()); err == nil { t.Fatal("expected error opening a directory") } // A missing parent directory is not created path := filepath.Join(t.TempDir(), "missing", "x.db") _, err := OpenDatabase(context.Background(), path, traceDiscard()) if err == nil || !strings.Contains(err.Error(), "initialize database schema") { t.Fatalf("expected schema error, got %v", err) } } func TestInitializeSchemaIdempotent(t *testing.T) { t.Parallel() path := testDBPath(t) for i := 0; i < 3; i++ { db, err := OpenDatabase(context.Background(), path, traceDiscard()) if err != nil { t.Fatalf("open #%d: %v", i, err) } if i == 0 { mustExec(t, db, `INSERT INTO tokens VALUES ('keep', 'd', 1, '2026-01-01T00:00:00Z', NULL)`) } if err := InitializeSchema(context.Background(), db, traceDiscard()); err != nil { t.Fatalf("re-init #%d: %v", i, err) } if n := countRows(t, db, "tokens"); n != 1 { t.Fatalf("re-init #%d lost data: %d rows", i, n) } db.Close() } } func TestInitializeSchemaError(t *testing.T) { t.Parallel() db := openTestDB(t) ctx, cancel := context.WithCancel(context.Background()) cancel() if err := InitializeSchema(ctx, db, traceDiscard()); err == nil { t.Fatal("expected error with cancelled context") } } // tableColumns returns name -> "type|notnull|pk" for a table func tableColumns(t *testing.T, db *sql.DB, table string) map[string]string { t.Helper() rows, err := db.Query(`SELECT name, type, "notnull", pk FROM pragma_table_info(?)`, table) if err != nil { t.Fatal(err) } defer rows.Close() cols := map[string]string{} for rows.Next() { var name, typ string var notnull, pk int if err := rows.Scan(&name, &typ, ¬null, &pk); err != nil { t.Fatal(err) } cols[name] = strings.Join([]string{typ, map[int]string{0: "null", 1: "notnull"}[notnull], map[int]string{0: "", 1: "pk"}[pk]}, "|") } return cols } func TestSchemaColumns(t *testing.T) { t.Parallel() db := openTestDB(t) tokens := tableColumns(t, db, "tokens") wantTokens := map[string]string{ "token": "TEXT|notnull|pk", "description": "TEXT|notnull|", "active": "INTEGER|notnull|", "created_at": "TEXT|notnull|", "deleted_at": "TEXT|null|", } if !reflect.DeepEqual(tokens, wantTokens) { t.Fatalf("tokens columns = %v", tokens) } ops := tableColumns(t, db, "operations") wantOps := map[string]string{ "token": "TEXT|notnull|", "origin": "TEXT|notnull|", "operation": "TEXT|notnull|", "time": "TEXT|notnull|", } if !reflect.DeepEqual(ops, wantOps) { t.Fatalf("operations columns = %v", ops) } var table, from, to string if err := db.QueryRow(`SELECT "table", "from", "to" FROM pragma_foreign_key_list('operations')`).Scan(&table, &from, &to); err != nil { t.Fatal(err) } if table != "tokens" || from != "token" || to != "token" { t.Fatalf("foreign key = %s(%s) -> %s", table, from, to) } // The description default is the empty string mustExec(t, db, `INSERT INTO tokens (token, active, created_at) VALUES ('d', 1, 'x')`) var desc string if err := db.QueryRow(`SELECT description FROM tokens WHERE token = 'd'`).Scan(&desc); err != nil || desc != "" { t.Fatalf("default description = %q, %v", desc, err) } } func TestSchemaPrimaryKey(t *testing.T) { t.Parallel() db := openTestDB(t) mustExec(t, db, `INSERT INTO tokens VALUES ('dup', '', 1, 'x', NULL)`) _, err := db.Exec(`INSERT INTO tokens VALUES ('dup', 'other', 1, 'y', NULL)`) if !isSQLiteCode(err, sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY) { t.Fatalf("expected primary key violation, got %v", err) } // Case matters: the primary key is case-sensitive mustExec(t, db, `INSERT INTO tokens VALUES ('DUP', '', 1, 'x', NULL)`) // NULL token is rejected if _, err := db.Exec(`INSERT INTO tokens VALUES (NULL, '', 1, 'x', NULL)`); err == nil { t.Fatal("NULL token accepted") } } func TestSchemaActiveCheck(t *testing.T) { t.Parallel() db := openTestDB(t) for i, v := range []any{0, 1} { if _, err := db.Exec(`INSERT INTO tokens VALUES (?, '', ?, 'x', NULL)`, "ok"+string(rune('0'+i)), v); err != nil { t.Errorf("active=%v rejected: %v", v, err) } } for i, v := range []any{2, -1, "yes", 0.5, nil} { _, err := db.Exec(`INSERT INTO tokens VALUES (?, '', ?, 'x', NULL)`, "bad"+string(rune('0'+i)), v) if err == nil { t.Errorf("active=%v accepted", v) } } } func TestSchemaForeignKeyEnforced(t *testing.T) { t.Parallel() db := openTestDB(t) // The pragma is on for every pooled connection, not just one var conns []*sql.Conn for i := 0; i < maxOpenConns; i++ { c, err := db.Conn(context.Background()) if err != nil { t.Fatal(err) } conns = append(conns, c) var fk int if err := c.QueryRowContext(context.Background(), `PRAGMA foreign_keys`).Scan(&fk); err != nil || fk != 1 { t.Fatalf("conn %d: foreign_keys = %d, %v", i, fk, err) } } for _, c := range conns { c.Close() } _, err := db.Exec(`INSERT INTO operations VALUES ('missing', '127.0.0.1', 'GET containers', 'x')`) if !isSQLiteCode(err, sqlite3.SQLITE_CONSTRAINT_FOREIGNKEY) { t.Fatalf("expected foreign key violation, got %v", err) } mustExec(t, db, `INSERT INTO tokens VALUES ('t', '', 1, 'x', NULL)`) mustExec(t, db, `INSERT INTO operations VALUES ('t', '127.0.0.1', 'GET containers', 'x')`) // A referenced token cannot be removed if _, err := db.Exec(`DELETE FROM tokens WHERE token = 't'`); !isSQLiteCode(err, sqlite3.SQLITE_CONSTRAINT_FOREIGNKEY) { t.Fatalf("expected foreign key violation on delete, got %v", err) } } func TestDatabasePragmas(t *testing.T) { t.Parallel() db := openTestDB(t) var mode string var busy int if err := db.QueryRow(`PRAGMA journal_mode`).Scan(&mode); err != nil || mode != "wal" { t.Fatalf("journal_mode = %q, %v", mode, err) } if err := db.QueryRow(`PRAGMA busy_timeout`).Scan(&busy); err != nil || busy != 5000 { t.Fatalf("busy_timeout = %d, %v", busy, err) } } func TestDeletedAtLifecycle(t *testing.T) { t.Parallel() db := openTestDB(t) store := NewTokenStore(db, traceDiscard(), newFakeClock(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), time.Second).Now, seqGenerator("tok")) if _, err := store.CreateToken(context.Background(), ""); err != nil { t.Fatal(err) } var deleted sql.NullString if err := db.QueryRow(`SELECT deleted_at FROM tokens WHERE token = 'tok'`).Scan(&deleted); err != nil || deleted.Valid { t.Fatalf("deleted_at should start NULL: %+v, %v", deleted, err) } if err := store.DeactivateToken(context.Background(), "tok"); err != nil { t.Fatal(err) } if err := db.QueryRow(`SELECT deleted_at FROM tokens WHERE token = 'tok'`).Scan(&deleted); err != nil || deleted.String != "2026-01-02T03:04:06Z" { t.Fatalf("deleted_at after delete = %+v, %v", deleted, err) } } func TestConcurrentWritersAcrossPools(t *testing.T) { t.Parallel() // Two independent pools on the same file emulate the server and the CLI writing at once path := testDBPath(t) open := func() *sql.DB { db, err := OpenDatabase(context.Background(), path, traceDiscard()) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return db } a, b := open(), open() mustExec(t, a, `INSERT INTO tokens VALUES ('t', '', 1, 'x', NULL)`) var wg sync.WaitGroup errs := make(chan error, 400) for _, db := range []*sql.DB{a, b} { audit := NewAuditStore(db, traceDiscard(), time.Now) store := NewTokenStore(db, traceDiscard(), time.Now, GenerateToken) for g := 0; g < 4; g++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < 25; i++ { if err := audit.RecordOperation(context.Background(), "t", "127.0.0.1", operationGetContainers); err != nil { errs <- err } tok, err := store.CreateToken(context.Background(), "c") if err != nil { errs <- err continue } if err := store.DeactivateToken(context.Background(), tok.Token); err != nil { errs <- err } } }() } } wg.Wait() close(errs) for err := range errs { t.Errorf("concurrent write failed: %v", err) } if n := countRows(t, a, "operations"); n != 200 { t.Fatalf("operations = %d, want 200", n) } if n := countRows(t, a, "tokens"); n != 201 { t.Fatalf("tokens = %d, want 201", n) } }