Files
at-container-registry/pkg/appview/db/device_store_scan_test.go
T
Evan JarrettandClaude Opus 5 8d7ccd7cb7 apply go fix modernizations across the workspace
`go fix` carries the modernize analyzers now, and the tree had drifted behind
them. This is the mechanical result, reviewed rather than trusted: the tool is
capable of rewriting code into something that no longer tests or does what it
did, so every non-test change was read individually and the concurrency-bearing
packages were re-run under -race.

Production code, four changes, all semantics-preserving:

  - leases/manager.go: wg.Add(1) + go + defer wg.Done() becomes wg.Go. The
    comment above that function turns on Add happening before the goroutine
    starts, so that a Wait cannot return before the worker has run. wg.Go does
    the Add synchronously on the calling goroutine, so the invariant it
    describes still holds.
  - auth/token/handler.go: strings.Fields -> strings.FieldsSeq, same splitting,
    iterated rather than allocated.
  - hold/gc/gc.go: a hand-written map copy -> maps.Copy.
  - hold/pds/scan_broadcaster.go: three-clause loop -> range over int.

The rest are tests. The one worth naming is carstore_contention_test.go, where a
careless rewrite could have quietly stopped exercising contention: go fix
converted the reader and side-table goroutines to loopWG.Go but correctly
declined to touch the writer loop, which passes its index as a parameter. The
writer/reader/side-table shape and the stop channel are unchanged, so the test
still contends over the same carstore transactions.

Verified: go build for hold and appview, `make lint` 0 issues, the deploy and
credential-helper modules 0 issues, `make test` green across all 43 packages,
and -race green on leases, hold/pds, hold/gc and auth/token. The scanner module's
two lint findings are unchanged from HEAD and are in files go fix never touched.

Kept separate from the HTTP/2 commit so that one stays readable, and so this can
be reverted on its own if a modernization turns out to matter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TA9D4DjaLZTvzQ7dJbu4eg
2026-09-08 22:38:01 -05:00

119 lines
4.4 KiB
Go

package db
import (
"fmt"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
)
// scanGuardCost makes a single bcrypt comparison expensive enough to be visible
// in wall-clock time. The defect being guarded is O(n) bcrypt over the devices
// table, so the only way to observe it is to make each comparison cost
// something; at the production cost of 10 the difference between "scanned six
// rows" and "scanned none" is lost in test noise.
const scanGuardCost = 13
// seedIndexedDeviceRows inserts n devices that are already indexed
// (secret_lookup populated) and whose bcrypt hashes are deliberately expensive.
//
// Only one hash is generated: the rest are copies with a mutated final
// character. bcrypt reads its cost from the hash prefix and runs the full key
// derivation before comparing, so a mutated tail costs exactly as much as a
// genuine hash and then fails to match — which is what a scan over non-matching
// rows does anyway.
func seedIndexedDeviceRows(t *testing.T, store *DeviceStore, did, handle string, n int) {
t.Helper()
base, err := bcrypt.GenerateFromPassword([]byte("filler-secret"), scanGuardCost)
if err != nil {
t.Fatalf("GenerateFromPassword: %v", err)
}
for i := range n {
hash := append([]byte(nil), base...)
// Vary the tail so secret_hash stays UNIQUE, keeping the base64 alphabet.
hash[len(hash)-1] = byte('a' + (i % 26))
if _, err := store.db.Exec(`
INSERT INTO devices (id, did, handle, name, secret_hash, secret_lookup,
ip_address, user_agent, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
`,
fmt.Sprintf("filler-device-%d", i), did, handle, fmt.Sprintf("Filler %d", i),
string(hash), deviceSecretLookup(fmt.Sprintf("atcr_device_filler_%d", i)),
"192.168.1.1", "Test Agent",
); err != nil {
t.Fatalf("seed device %d: %v", i, err)
}
}
}
// TestDeviceStore_IndexedRowsAreNeverBcryptCompared is the real regression guard
// for the O(n) bcrypt scan.
//
// Its sibling TestDeviceStore_ValidateDoesNotScanIndexedRows does not catch the
// defect: it asserts every row is indexed and that an unknown secret errors,
// and both of those hold whether or not the code scans. Removing the
// `WHERE secret_lookup IS NULL` filter from the fallback query leaves it green.
//
// What actually has to be true is that indexed rows are never handed to bcrypt.
// That is only observable in time, so the assertion is a deadline: six expensive
// rows cost ~1.8s to scan and ~0ms to skip.
func TestDeviceStore_IndexedRowsAreNeverBcryptCompared(t *testing.T) {
const fillerRows = 6
store := setupTestDB(t)
createTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
seedIndexedDeviceRows(t, store, "did:plc:alice123", "alice.bsky.social", fillerRows)
// A real device, indexed at creation like every post-0028 row.
secret := newDeviceForTest(t, store, "did:plc:alice123", "alice.bsky.social", "My Device")
var unindexed int
if err := store.db.QueryRow(
`SELECT COUNT(*) FROM devices WHERE secret_lookup IS NULL OR secret_lookup = ''`,
).Scan(&unindexed); err != nil {
t.Fatalf("count unindexed: %v", err)
}
if unindexed != 0 {
t.Fatalf("fixture is wrong: %d rows unindexed, so the scan has legitimate work to do", unindexed)
}
// Anything above one expensive comparison means rows were scanned. The
// budget is generous against a ~1.8s full scan so this does not flake on a
// loaded machine.
const budget = 400 * time.Millisecond
t.Run("unknown secret", func(t *testing.T) {
start := time.Now()
_, err := store.ValidateDeviceSecret("atcr_device_no_such_secret")
elapsed := time.Since(start)
if err == nil {
t.Error("expected an error for an unknown secret")
}
// The worst case for the old code: no row ever matches, so it paid for
// every bcrypt comparison in the table before giving up.
if elapsed > budget {
t.Errorf("rejecting an unknown secret took %v (budget %v) — indexed rows are being bcrypt-compared, which is the O(n) scan this fix removed", elapsed, budget)
}
})
t.Run("known secret", func(t *testing.T) {
start := time.Now()
device, err := store.ValidateDeviceSecret(secret)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("ValidateDeviceSecret() error = %v", err)
}
if device.Name != "My Device" {
t.Errorf("Name = %v, want My Device", device.Name)
}
if elapsed > budget {
t.Errorf("resolving an indexed secret took %v (budget %v) — it is not using the index", elapsed, budget)
}
})
}