DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
578 lines
19 KiB
Go
578 lines
19 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var tokenAlphabet = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
|
|
|
var t0 = time.Date(2026, 10, 5, 12, 0, 0, 123456789, time.UTC)
|
|
|
|
func TestGenerateToken(t *testing.T) {
|
|
t.Parallel()
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 1000; i++ {
|
|
tok, err := GenerateToken()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tok == "" {
|
|
t.Fatal("empty token")
|
|
}
|
|
if len(tok) != tokenLength {
|
|
t.Fatalf("length %d, want %d", len(tok), tokenLength)
|
|
}
|
|
if !tokenAlphabet.MatchString(tok) {
|
|
t.Fatalf("token %q outside the URL-safe alphabet", tok)
|
|
}
|
|
if seen[tok] {
|
|
t.Fatalf("duplicate token after %d generations", i)
|
|
}
|
|
seen[tok] = true
|
|
}
|
|
}
|
|
|
|
func TestGenerateTokenFromDeterministicReader(t *testing.T) {
|
|
t.Parallel()
|
|
tok, err := generateTokenFrom(strings.NewReader(strings.Repeat("\xff", tokenBytes)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tok != strings.Repeat("_", 42)+"8" {
|
|
t.Fatalf("unexpected encoding %q", tok)
|
|
}
|
|
}
|
|
|
|
func TestGenerateTokenReaderError(t *testing.T) {
|
|
t.Parallel()
|
|
if _, err := generateTokenFrom(strings.NewReader("short")); !errors.Is(err, io.ErrUnexpectedEOF) {
|
|
t.Fatalf("expected short-read error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// newTestStore returns a store with a deterministic clock (1ms steps) and generator
|
|
func newTestStore(t *testing.T, gen func() (string, error)) (*TokenStore, *sql.DB) {
|
|
t.Helper()
|
|
db := openTestDB(t)
|
|
return NewTokenStore(db, traceDiscard(), newFakeClock(t0, time.Millisecond).Now, gen), db
|
|
}
|
|
|
|
func TestCreateTokenDescriptions(t *testing.T) {
|
|
t.Parallel()
|
|
descriptions := []string{
|
|
"",
|
|
"ci deploy token",
|
|
" leading and trailing ",
|
|
"quotes ' \" ` and $(rm -rf /); DROP TABLE tokens; --",
|
|
"line1\nline2\r\n\ttab",
|
|
"unicode 日本語 🚀 full",
|
|
"ctl \x01\x1b[31m\x7f",
|
|
string([]byte{0xff, 0xfe, 'x'}),
|
|
"nul\x00byte",
|
|
strings.Repeat("long", 25000),
|
|
}
|
|
store, db := newTestStore(t, GenerateToken)
|
|
for _, desc := range descriptions {
|
|
tok, err := store.CreateToken(context.Background(), desc)
|
|
if err != nil {
|
|
t.Fatalf("CreateToken(%q): %v", desc, err)
|
|
}
|
|
if !tok.Active || tok.DeletedAt != nil || tok.Description != desc || !tokenAlphabet.MatchString(tok.Token) {
|
|
t.Fatalf("unexpected token %+v", tok)
|
|
}
|
|
var stored string
|
|
if err := db.QueryRow(`SELECT description FROM tokens WHERE token = ?`, tok.Token).Scan(&stored); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored != desc {
|
|
t.Fatalf("description not stored verbatim: %q != %q", stored, desc)
|
|
}
|
|
got, err := store.GetToken(context.Background(), tok.Token)
|
|
if err != nil || got.Description != desc {
|
|
t.Fatalf("GetToken description = %q, %v", got.Description, err)
|
|
}
|
|
}
|
|
if n := countRows(t, db, "tokens"); n != len(descriptions) {
|
|
t.Fatalf("rows = %d", n)
|
|
}
|
|
// The SQL-injection payload did nothing
|
|
if n := countRows(t, db, "operations"); n != 0 {
|
|
t.Fatalf("operations = %d", n)
|
|
}
|
|
}
|
|
|
|
func TestCreateTokenTimestamps(t *testing.T) {
|
|
t.Parallel()
|
|
local := time.FixedZone("UTC+5", 5*3600)
|
|
db := openTestDB(t)
|
|
store := NewTokenStore(db, traceDiscard(), func() time.Time { return time.Date(2026, 3, 4, 10, 0, 0, 500, local) }, seqGenerator("a"))
|
|
tok, err := store.CreateToken(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var created string
|
|
if err := db.QueryRow(`SELECT created_at FROM tokens WHERE token = 'a'`).Scan(&created); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if created != "2026-03-04T05:00:00.0000005Z" {
|
|
t.Fatalf("created_at = %q", created)
|
|
}
|
|
if tok.CreatedAt.Location() != time.UTC || !tok.CreatedAt.Equal(time.Date(2026, 3, 4, 5, 0, 0, 500, time.UTC)) {
|
|
t.Fatalf("CreatedAt = %v", tok.CreatedAt)
|
|
}
|
|
if _, err := time.Parse(time.RFC3339Nano, created); err != nil {
|
|
t.Fatalf("not RFC3339Nano: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateTokenCollisionRetry(t *testing.T) {
|
|
t.Parallel()
|
|
log, buf := newTestLogger(LevelTrace)
|
|
db := openTestDB(t)
|
|
store := NewTokenStore(db, log, newFakeClock(t0, time.Millisecond).Now, seqGenerator("same", "same", "same", "other"))
|
|
if _, err := store.CreateToken(context.Background(), "first"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tok, err := store.CreateToken(context.Background(), "second")
|
|
if err != nil {
|
|
t.Fatalf("expected retry to succeed: %v", err)
|
|
}
|
|
if tok.Token != "other" {
|
|
t.Fatalf("token = %q", tok.Token)
|
|
}
|
|
if c := strings.Count(buf.String(), "generated token collided"); c != 2 {
|
|
t.Fatalf("collision logs = %d", c)
|
|
}
|
|
// The colliding row was not modified
|
|
first, err := store.GetToken(context.Background(), "same")
|
|
if err != nil || first.Description != "first" {
|
|
t.Fatalf("existing row changed: %+v, %v", first, err)
|
|
}
|
|
}
|
|
|
|
func TestCreateTokenCollisionExhausted(t *testing.T) {
|
|
t.Parallel()
|
|
gen := func() (string, error) { return "fixed", nil }
|
|
store, db := newTestStore(t, gen)
|
|
if _, err := store.CreateToken(context.Background(), ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.CreateToken(context.Background(), ""); !errors.Is(err, ErrTokenCollision) {
|
|
t.Fatalf("expected ErrTokenCollision, got %v", err)
|
|
}
|
|
if n := countRows(t, db, "tokens"); n != 1 {
|
|
t.Fatalf("rows = %d", n)
|
|
}
|
|
}
|
|
|
|
func TestCreateTokenGeneratorError(t *testing.T) {
|
|
t.Parallel()
|
|
store, _ := newTestStore(t, func() (string, error) { return "", errors.New("no entropy") })
|
|
if _, err := store.CreateToken(context.Background(), ""); err == nil || !strings.Contains(err.Error(), "no entropy") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateTokenInsertError(t *testing.T) {
|
|
t.Parallel()
|
|
store, db := newTestStore(t, GenerateToken)
|
|
db.Close()
|
|
if _, err := store.CreateToken(context.Background(), ""); err == nil || !strings.Contains(err.Error(), "insert token") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListTokensEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
store, _ := newTestStore(t, GenerateToken)
|
|
for _, activeOnly := range []bool{false, true} {
|
|
tokens, err := store.ListTokens(context.Background(), activeOnly)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tokens == nil || len(tokens) != 0 {
|
|
t.Fatalf("activeOnly=%v: expected empty non-nil slice, got %#v", activeOnly, tokens)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListTokensOne(t *testing.T) {
|
|
t.Parallel()
|
|
store, _ := newTestStore(t, seqGenerator("only"))
|
|
created, err := store.CreateToken(context.Background(), "d")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tokens, err := store.ListTokens(context.Background(), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tokens) != 1 || tokens[0].Token != "only" || tokens[0].Description != "d" || !tokens[0].Active ||
|
|
!tokens[0].CreatedAt.Equal(created.CreatedAt) || tokens[0].DeletedAt != nil {
|
|
t.Fatalf("got %+v", tokens)
|
|
}
|
|
}
|
|
|
|
func TestListTokensManyOrdering(t *testing.T) {
|
|
t.Parallel()
|
|
db := openTestDB(t)
|
|
store := NewTokenStore(db, traceDiscard(), time.Now, GenerateToken)
|
|
// Rows inserted out of order, with fraction lengths that break lexical ordering
|
|
// "y", "a" and "w" are inactive
|
|
rows := []struct {
|
|
token, created string
|
|
active int
|
|
}{
|
|
{"z", "2026-01-01T00:00:00.1Z", 1},
|
|
{"y", "2026-01-01T00:00:00Z", 0},
|
|
{"x", "2026-01-01T00:00:00.12Z", 1},
|
|
{"b", "2026-01-01T00:00:00.5Z", 1},
|
|
{"a", "2026-01-01T00:00:00.5Z", 0},
|
|
{"w", "2025-12-31T23:59:59.999999999Z", 0},
|
|
{"c", "2026-01-01T00:00:00.5Z", 1},
|
|
}
|
|
for _, r := range rows {
|
|
deleted := any(nil)
|
|
if r.active == 0 {
|
|
deleted = "2026-02-01T00:00:00Z"
|
|
}
|
|
mustExec(t, db, `INSERT INTO tokens VALUES (?, '', ?, ?, ?)`, r.token, r.active, r.created, deleted)
|
|
}
|
|
for activeOnly, want := range map[bool]string{false: "w,y,z,x,a,b,c", true: "z,x,b,c"} {
|
|
tokens, err := store.ListTokens(context.Background(), activeOnly)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got []string
|
|
for _, tok := range tokens {
|
|
got = append(got, tok.Token)
|
|
}
|
|
if strings.Join(got, ",") != want {
|
|
t.Fatalf("activeOnly=%v: order = %v, want %s", activeOnly, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListTokensMany(t *testing.T) {
|
|
t.Parallel()
|
|
store, _ := newTestStore(t, GenerateToken)
|
|
var want []string
|
|
for i := 0; i < 200; i++ {
|
|
tok, err := store.CreateToken(context.Background(), fmt.Sprint(i))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want = append(want, tok.Token)
|
|
}
|
|
for i := 0; i < 200; i += 3 {
|
|
if err := store.DeactivateToken(context.Background(), want[i]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
tokens, err := store.ListTokens(context.Background(), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tokens) != 200 {
|
|
t.Fatalf("len = %d", len(tokens))
|
|
}
|
|
for i, tok := range tokens {
|
|
inactive := i%3 == 0
|
|
if tok.Token != want[i] || tok.Description != fmt.Sprint(i) || tok.Active == inactive || (tok.DeletedAt != nil) != inactive {
|
|
t.Fatalf("row %d = %+v", i, tok)
|
|
}
|
|
}
|
|
|
|
// Only the active ones, same relative order
|
|
active, err := store.ListTokens(context.Background(), true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wantActive []Token
|
|
for i, tok := range tokens {
|
|
if i%3 != 0 {
|
|
wantActive = append(wantActive, tok)
|
|
}
|
|
}
|
|
if len(active) != len(wantActive) {
|
|
t.Fatalf("active len = %d, want %d", len(active), len(wantActive))
|
|
}
|
|
for i, tok := range active {
|
|
if tok.Token != wantActive[i].Token || !tok.Active || tok.DeletedAt != nil {
|
|
t.Fatalf("active row %d = %+v", i, tok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListTokensErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Query error: closed database
|
|
store, db := newTestStore(t, GenerateToken)
|
|
db.Close()
|
|
for _, activeOnly := range []bool{false, true} {
|
|
if _, err := store.ListTokens(context.Background(), activeOnly); err == nil || !strings.Contains(err.Error(), "query tokens") {
|
|
t.Fatalf("activeOnly=%v: got %v", activeOnly, err)
|
|
}
|
|
}
|
|
|
|
// Malformed rows written by something else
|
|
for name, row := range map[string][]any{
|
|
"bad created_at": {"t", "", 1, "yesterday", nil},
|
|
"bad deleted_at": {"t", "", 0, "2026-01-01T00:00:00Z", "tomorrow"},
|
|
} {
|
|
store, db := newTestStore(t, GenerateToken)
|
|
mustExec(t, db, `INSERT INTO tokens VALUES (?, ?, ?, ?, ?)`, row...)
|
|
if _, err := store.ListTokens(context.Background(), false); err == nil || !strings.Contains(err.Error(), "parse stored timestamp") {
|
|
t.Errorf("%s: got %v", name, err)
|
|
}
|
|
}
|
|
|
|
// The inactive malformed row is filtered out in SQL, before it is parsed
|
|
store, db = newTestStore(t, GenerateToken)
|
|
mustExec(t, db, `INSERT INTO tokens VALUES ('t', '', 0, '2026-01-01T00:00:00Z', 'tomorrow')`)
|
|
if tokens, err := store.ListTokens(context.Background(), true); err != nil || len(tokens) != 0 {
|
|
t.Fatalf("active only: got %v, %v", tokens, err)
|
|
}
|
|
}
|
|
|
|
// cancelOnRecord is a slog handler that cancels a context when it sees a record with the given message
|
|
type cancelOnRecord struct {
|
|
slog.Handler
|
|
msg string
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (h cancelOnRecord) Enabled(context.Context, slog.Level) bool { return true }
|
|
|
|
func (h cancelOnRecord) Handle(ctx context.Context, r slog.Record) error {
|
|
if r.Message == h.msg {
|
|
h.cancel()
|
|
// Give database/sql and the driver time to observe the cancellation
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestListTokensIterationError(t *testing.T) {
|
|
t.Parallel()
|
|
db := openTestDB(t)
|
|
for i := 0; i < 500; i++ {
|
|
mustExec(t, db, `INSERT INTO tokens VALUES (?, '', 1, '2026-01-01T00:00:00Z', NULL)`, fmt.Sprint(i))
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
log := slog.New(cancelOnRecord{msg: "row read", cancel: cancel})
|
|
store := NewTokenStore(db, log, time.Now, GenerateToken)
|
|
if _, err := store.ListTokens(ctx, false); err == nil || !strings.Contains(err.Error(), "iterate token rows") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
// newLegacySchemaStore creates a database whose tables were pre-created without constraints,
|
|
// so rows the real schema would reject can be used to exercise defensive error paths
|
|
func newLegacySchemaStore(t *testing.T, ddl string) (*TokenStore, *sql.DB) {
|
|
t.Helper()
|
|
path := testDBPath(t)
|
|
raw, err := sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustExec(t, raw, ddl)
|
|
raw.Close()
|
|
db, err := OpenDatabase(context.Background(), path, traceDiscard())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
return NewTokenStore(db, traceDiscard(), newFakeClock(t0, time.Millisecond).Now, GenerateToken), db
|
|
}
|
|
|
|
func TestTokenScanErrors(t *testing.T) {
|
|
t.Parallel()
|
|
store, db := newLegacySchemaStore(t, `CREATE TABLE tokens (token TEXT, description TEXT, active, created_at TEXT, deleted_at TEXT)`)
|
|
mustExec(t, db, `INSERT INTO tokens VALUES ('t', NULL, 'not-a-number', '2026-01-01T00:00:00Z', NULL)`)
|
|
if _, err := store.ListTokens(context.Background(), false); err == nil || !strings.Contains(err.Error(), "read token row") {
|
|
t.Fatalf("ListTokens: got %v", err)
|
|
}
|
|
if _, err := store.GetToken(context.Background(), "t"); err == nil || !strings.Contains(err.Error(), "query token") {
|
|
t.Fatalf("GetToken: got %v", err)
|
|
}
|
|
if err := store.DeactivateToken(context.Background(), "t"); err == nil || !strings.Contains(err.Error(), "query token") {
|
|
t.Fatalf("DeactivateToken: got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetToken(t *testing.T) {
|
|
t.Parallel()
|
|
store, _ := newTestStore(t, seqGenerator("tok-a", "tok-b"))
|
|
a, _ := store.CreateToken(context.Background(), "A")
|
|
if _, err := store.CreateToken(context.Background(), "B"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Existing
|
|
got, err := store.GetToken(context.Background(), "tok-a")
|
|
if err != nil || got.Token != "tok-a" || got.Description != "A" || !got.Active || !got.CreatedAt.Equal(a.CreatedAt) || got.DeletedAt != nil {
|
|
t.Fatalf("got %+v, %v", got, err)
|
|
}
|
|
|
|
// Unknown, near-match, case change, whitespace
|
|
for _, v := range []string{"", "tok-c", "TOK-A", "tok-a ", " tok-a", "tok-", "tok-a'--", "%"} {
|
|
if _, err := store.GetToken(context.Background(), v); !errors.Is(err, ErrTokenNotFound) {
|
|
t.Errorf("GetToken(%q) = %v", v, err)
|
|
}
|
|
}
|
|
|
|
// Inactive
|
|
if err := store.DeactivateToken(context.Background(), "tok-b"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err = store.GetToken(context.Background(), "tok-b")
|
|
if err != nil || got.Active || got.DeletedAt == nil {
|
|
t.Fatalf("inactive: %+v, %v", got, err)
|
|
}
|
|
|
|
// Query error
|
|
store.db.Close()
|
|
if _, err := store.GetToken(context.Background(), "tok-a"); err == nil || errors.Is(err, ErrTokenNotFound) {
|
|
t.Fatalf("closed db: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeactivateToken(t *testing.T) {
|
|
t.Parallel()
|
|
store, db := newTestStore(t, seqGenerator("tok-a", "tok-b"))
|
|
if _, err := store.CreateToken(context.Background(), ""); err != nil { // created at t0
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.CreateToken(context.Background(), ""); err != nil { // created at t0+1ms
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.DeactivateToken(context.Background(), "tok-a"); err != nil { // deleted at t0+2ms
|
|
t.Fatal(err)
|
|
}
|
|
var active int
|
|
var deleted sql.NullString
|
|
if err := db.QueryRow(`SELECT active, deleted_at FROM tokens WHERE token = 'tok-a'`).Scan(&active, &deleted); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if active != 0 || deleted.String != "2026-10-05T12:00:00.125456789Z" {
|
|
t.Fatalf("active=%d deleted_at=%+v", active, deleted)
|
|
}
|
|
// The other token is untouched
|
|
if err := db.QueryRow(`SELECT active, deleted_at FROM tokens WHERE token = 'tok-b'`).Scan(&active, &deleted); err != nil || active != 1 || deleted.Valid {
|
|
t.Fatalf("tok-b changed: %d %+v %v", active, deleted, err)
|
|
}
|
|
|
|
// Already inactive: error, and deleted_at keeps its original value
|
|
if err := store.DeactivateToken(context.Background(), "tok-a"); !errors.Is(err, ErrTokenInactive) {
|
|
t.Fatalf("second delete: %v", err)
|
|
}
|
|
if err := db.QueryRow(`SELECT deleted_at FROM tokens WHERE token = 'tok-a'`).Scan(&deleted); err != nil || deleted.String != "2026-10-05T12:00:00.125456789Z" {
|
|
t.Fatalf("deleted_at changed: %+v", deleted)
|
|
}
|
|
|
|
// Unknown
|
|
if err := store.DeactivateToken(context.Background(), "nope"); !errors.Is(err, ErrTokenNotFound) {
|
|
t.Fatalf("unknown: %v", err)
|
|
}
|
|
if n := countRows(t, db, "tokens"); n != 2 {
|
|
t.Fatalf("rows = %d", n)
|
|
}
|
|
}
|
|
|
|
func TestDeactivateTokenErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Begin error
|
|
store, db := newTestStore(t, GenerateToken)
|
|
db.Close()
|
|
if err := store.DeactivateToken(context.Background(), "x"); err == nil || !strings.Contains(err.Error(), "begin transaction") {
|
|
t.Fatalf("begin: %v", err)
|
|
}
|
|
|
|
// Update error: a trigger rejects the update
|
|
store, db = newTestStore(t, seqGenerator("t"))
|
|
if _, err := store.CreateToken(context.Background(), ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustExec(t, db, `CREATE TRIGGER no_update BEFORE UPDATE ON tokens BEGIN SELECT RAISE(ABORT, 'refused'); END`)
|
|
if err := store.DeactivateToken(context.Background(), "t"); err == nil || !strings.Contains(err.Error(), "update token") {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
var active int
|
|
if err := db.QueryRow(`SELECT active FROM tokens WHERE token = 't'`).Scan(&active); err != nil || active != 1 {
|
|
t.Fatalf("token changed after failed update: %d %v", active, err)
|
|
}
|
|
|
|
// Commit error: a deferred foreign key on deleted_at is violated only at COMMIT
|
|
store, db = newLegacySchemaStore(t, `
|
|
CREATE TABLE parents (id TEXT PRIMARY KEY);
|
|
CREATE TABLE tokens (
|
|
token TEXT PRIMARY KEY NOT NULL, description TEXT NOT NULL DEFAULT '',
|
|
active INTEGER NOT NULL, created_at TEXT NOT NULL,
|
|
deleted_at TEXT NULL REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED)`)
|
|
mustExec(t, db, `INSERT INTO tokens VALUES ('t', '', 1, '2026-01-01T00:00:00Z', NULL)`)
|
|
if err := store.DeactivateToken(context.Background(), "t"); err == nil || !strings.Contains(err.Error(), "commit transaction") {
|
|
t.Fatalf("commit: %v", err)
|
|
}
|
|
if err := db.QueryRow(`SELECT active FROM tokens WHERE token = 't'`).Scan(&active); err != nil || active != 1 {
|
|
t.Fatalf("token changed after failed commit: %d %v", active, err)
|
|
}
|
|
}
|
|
|
|
func TestTokenStoreTraceRedaction(t *testing.T) {
|
|
t.Parallel()
|
|
log, buf := newTestLogger(LevelTrace)
|
|
db := openTestDB(t)
|
|
const secret = "SECRET_TOKEN_VALUE_0123456789abcdefghijklmno"
|
|
store := NewTokenStore(db, log, newFakeClock(t0, time.Millisecond).Now, seqGenerator(secret))
|
|
if _, err := store.CreateToken(context.Background(), "my description"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.GetToken(context.Background(), secret); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ListTokens(context.Background(), false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.DeactivateToken(context.Background(), secret); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = store.DeactivateToken(context.Background(), secret)
|
|
_, _ = store.GetToken(context.Background(), secret+"x")
|
|
_ = store.DeactivateToken(context.Background(), secret+"x")
|
|
out := buf.String()
|
|
if strings.Contains(out, secret) || strings.Contains(out, secret[:12]) {
|
|
t.Fatalf("token secret leaked into trace output:\n%s", out)
|
|
}
|
|
id := tokenID(secret)
|
|
for _, want := range []string{
|
|
`msg="function entry" func=TokenStore.CreateToken`,
|
|
`msg="function exit" func=TokenStore.CreateToken`,
|
|
`msg="row inserted" table=tokens token_id=` + id + ` description="my description" active=true created_at=2026-10-05T12:00:00.123456789Z deleted_at=<nil>`,
|
|
`msg="row read" table=tokens token_id=` + id,
|
|
`msg="row updated" table=tokens token_id=` + id + ` active_from=true active_to=false deleted_at=`,
|
|
`msg="row already inactive" table=tokens token_id=` + id,
|
|
`msg="row not found" table=tokens token_id=` + tokenID(secret+"x"),
|
|
`func=TokenStore.ListTokens`, `func=TokenStore.GetToken`, `func=TokenStore.DeactivateToken`,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("missing trace record %q", want)
|
|
}
|
|
}
|
|
}
|