Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358package db
import ( "context" "database/sql" "path/filepath" "slices" "testing" "time")
func createAndRegisterExecutor(t *testing.T, d *DB, name, tokenHash string, expiresAt *time.Time, labels []string) { t.Helper() if err := d.CreateExecutorToken(tokenHash, expiresAt); err != nil { t.Fatalf("CreateExecutorToken: %v", err) } if err := d.RegisterExecutor(name, tokenHash, labels); err != nil { t.Fatalf("RegisterExecutor: %v", err) }}
func TestExecutorTokenMustExistBeforeRegistration(t *testing.T) { d := newTestDB(t)
if err := d.RegisterExecutor("exec-1", "hash-1", nil); err == nil { t.Fatal("RegisterExecutor accepted an unknown token") } if err := d.CreateExecutorToken("hash-1", nil); err != nil { t.Fatalf("CreateExecutorToken: %v", err) } if _, _, ok, err := d.ResolveExecutorToken("hash-1"); err != nil || ok { t.Fatalf("ResolveExecutorToken(unregistered) = (ok=%v, err=%v), want (false, nil)", ok, err) } if err := d.RegisterExecutor("exec-1", "hash-1", nil); err != nil { t.Fatalf("RegisterExecutor: %v", err) } name, _, ok, err := d.ResolveExecutorToken("hash-1") if err != nil || !ok || name != "exec-1" { t.Fatalf("ResolveExecutorToken(registered) = (%q, %v, %v), want (exec-1, true, nil)", name, ok, err) }}
func TestRegisterExecutorIsIdempotentAndReconcilesLabels(t *testing.T) { d := newTestDB(t) createAndRegisterExecutor(t, d, "exec-1", "hash-1", nil, []string{"old"})
if err := d.RegisterExecutor("exec-1", "hash-1", []string{" arm64 ", "kvm", "arm64"}); err != nil { t.Fatalf("RegisterExecutor(repeat): %v", err) } name, labels, ok, err := d.ResolveExecutorToken("hash-1") if err != nil || !ok || name != "exec-1" { t.Fatalf("ResolveExecutorToken = (%q, %v, %v), want (exec-1, true, nil)", name, ok, err) } if want := []string{"arm64", "kvm"}; !slices.Equal(labels, want) { t.Fatalf("labels = %v, want %v", labels, want) }}
func TestRegisterExecutorRejectsCredentialConflicts(t *testing.T) { d := newTestDB(t) if err := d.CreateExecutorToken("hash-a", nil); err != nil { t.Fatalf("CreateExecutorToken(hash-a): %v", err) } if err := d.CreateExecutorToken("hash-b", nil); err != nil { t.Fatalf("CreateExecutorToken(hash-b): %v", err) } if err := d.RegisterExecutor("exec-1", "hash-a", nil); err != nil { t.Fatalf("RegisterExecutor: %v", err) }
if err := d.RegisterExecutor("exec-1", "hash-b", nil); err == nil { t.Fatal("RegisterExecutor replaced an existing executor's token") } if err := d.RegisterExecutor("exec-2", "hash-a", nil); err == nil { t.Fatal("RegisterExecutor assigned one token to two executor names") } if name, _, ok, err := d.ResolveExecutorToken("hash-a"); err != nil || !ok || name != "exec-1" { t.Fatalf("ResolveExecutorToken(hash-a) = (%q, %v, %v), want (exec-1, true, nil)", name, ok, err) } if _, _, ok, err := d.ResolveExecutorToken("hash-b"); err != nil || ok { t.Fatalf("ResolveExecutorToken(hash-b) = (ok=%v, err=%v), want (false, nil)", ok, err) }}
func TestRegisterExecutorRejectsExpiredToken(t *testing.T) { d := newTestDB(t) expired := time.Now().Add(-time.Hour) if err := d.CreateExecutorToken("hash-1", &expired); err != nil { t.Fatalf("CreateExecutorToken: %v", err) } if err := d.RegisterExecutor("exec-1", "hash-1", nil); err == nil { t.Fatal("RegisterExecutor accepted an expired token") }}
func TestResolveExecutorTokenMissAndHit(t *testing.T) { d := newTestDB(t)
name, _, ok, err := d.ResolveExecutorToken("no-such-hash") if err != nil || ok || name != "" { t.Fatalf("ResolveExecutorToken(miss) = (%q, %v, %v), want (\"\", false, nil)", name, ok, err) }
createAndRegisterExecutor(t, d, "exec-1", "hash-1", nil, nil) name, _, ok, err = d.ResolveExecutorToken("hash-1") if err != nil || !ok || name != "exec-1" { t.Fatalf("ResolveExecutorToken(hit) = (%q, %v, %v), want (exec-1, true, nil)", name, ok, err) }}
func TestRevokeExecutorToken(t *testing.T) { d := newTestDB(t) createAndRegisterExecutor(t, d, "exec-1", "hash-1", nil, nil)
deleted, err := d.RevokeExecutorToken("exec-1") if err != nil || !deleted { t.Fatalf("RevokeExecutorToken = (%v, %v), want (true, nil)", deleted, err) } if _, _, ok, err := d.ResolveExecutorToken("hash-1"); err != nil || ok { t.Fatalf("ResolveExecutorToken(revoked) = (ok=%v, err=%v), want (false, nil)", ok, err) } var tokens int if err := d.QueryRow(`select count(*) from mill_executor_tokens where token_hash = ?`, "hash-1").Scan(&tokens); err != nil { t.Fatalf("count revoked token: %v", err) } if tokens != 0 { t.Fatal("RevokeExecutorToken left the credential behind") } if deleted, err := d.RevokeExecutorToken("exec-1"); err != nil || deleted { t.Fatalf("RevokeExecutorToken(already gone) = (%v, %v), want (false, nil)", deleted, err) }}
func TestListExecutorRegistrations(t *testing.T) { d := newTestDB(t)
registrations, err := d.ListExecutorRegistrations() if err != nil || len(registrations) != 0 { t.Fatalf("ListExecutorRegistrations(empty) = (%d rows, %v), want (0, nil)", len(registrations), err) } for _, name := range []string{"charlie", "alice", "bob"} { createAndRegisterExecutor(t, d, name, "hash-"+name, nil, nil) } registrations, err = d.ListExecutorRegistrations() if err != nil { t.Fatalf("ListExecutorRegistrations: %v", err) } want := []string{"alice", "bob", "charlie"} if len(registrations) != len(want) { t.Fatalf("ListExecutorRegistrations = %d rows, want %d", len(registrations), len(want)) } for i := range want { if registrations[i].Name != want[i] { t.Fatalf("ListExecutorRegistrations[%d].Name = %q, want %q", i, registrations[i].Name, want[i]) } }}
func TestResolveExecutorTokenExpiry(t *testing.T) { cases := []struct { name string offset time.Duration noExpiry bool wantValid bool }{ {"future expiry resolves", time.Hour, false, true}, {"past expiry fails closed", -time.Hour, false, false}, {"nil expiry never expires", 0, true, true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { d := newTestDB(t) var expires *time.Time if !tc.noExpiry { exp := time.Now().Add(tc.offset) expires = &exp } if err := d.CreateExecutorToken("hash-1", expires); err != nil { t.Fatalf("CreateExecutorToken: %v", err) } if tc.wantValid { if err := d.RegisterExecutor("exec-1", "hash-1", nil); err != nil { t.Fatalf("RegisterExecutor: %v", err) } } else if _, err := d.Exec(`insert into mill_executors (name, token_hash) values (?, ?)`, "exec-1", "hash-1"); err != nil { t.Fatalf("insert expired registration: %v", err) } name, _, ok, err := d.ResolveExecutorToken("hash-1") if err != nil || ok != tc.wantValid { t.Fatalf("ResolveExecutorToken = (%q, %v, %v), want valid=%v", name, ok, err, tc.wantValid) } }) }}
func TestResolveExecutorTokenRejectsMalformedExpiry(t *testing.T) { d := newTestDB(t) if _, err := d.Exec(`insert into mill_executor_tokens (token_hash, expires_at) values (?, ?)`, "hash-1", "not-a-timestamp"); err != nil { t.Fatalf("insert malformed token: %v", err) } if _, err := d.Exec(`insert into mill_executors (name, token_hash) values (?, ?)`, "exec-1", "hash-1"); err != nil { t.Fatalf("insert registration: %v", err) } name, _, ok, err := d.ResolveExecutorToken("hash-1") if err == nil || ok || name != "" { t.Fatalf("ResolveExecutorToken = (%q, %v, %v), want (\"\", false, error)", name, ok, err) }}
func TestListExecutorRegistrationsSurfacesExpiry(t *testing.T) { d := newTestDB(t) exp := time.Now().Add(24 * time.Hour) createAndRegisterExecutor(t, d, "expiring", "hash-exp", &exp, nil) createAndRegisterExecutor(t, d, "forever", "hash-forever", nil, nil)
registrations, err := d.ListExecutorRegistrations() if err != nil { t.Fatalf("ListExecutorRegistrations: %v", err) } got := make(map[string]*time.Time, len(registrations)) for _, registration := range registrations { got[registration.Name] = registration.ExpiresAt } if got["forever"] != nil { t.Fatalf("forever.ExpiresAt = %v, want nil", got["forever"]) } if got["expiring"] == nil || got["expiring"].Unix() != exp.Unix() { t.Fatalf("expiring.ExpiresAt = %v, want unix %d", got["expiring"], exp.Unix()) }}
func TestLegacyExecutorTokensMigrateWithoutChangingIdentityState(t *testing.T) { path := filepath.Join(t.TempDir(), "spindle.db") legacy, err := sql.Open("sqlite3", path) if err != nil { t.Fatalf("open legacy db: %v", err) } expires := time.Now().UTC().Add(24 * time.Hour).Truncate(time.Second).Format(time.RFC3339) if _, err := legacy.Exec(` create table mill_executors ( name text primary key, token_hash text not null unique, created_at text not null, expires_at text, labels text, quarantine_reason text, quarantined_at text ); create table mill_executor_cursors ( node_id text not null, epoch text not null, acked_seqno integer not null, primary key (node_id, epoch) ); insert into mill_executors ( name, token_hash, created_at, expires_at, labels, quarantine_reason, quarantined_at ) values ('exec-1', 'hash-1', '2026-01-02T03:04:05Z', ?, '["arm64"]', 'old quarantine', '2026-01-03T00:00:00Z'); insert into mill_executor_cursors (node_id, epoch, acked_seqno) values ('exec-1', 'epoch-1', 42); `, expires); err != nil { legacy.Close() t.Fatalf("seed legacy db: %v", err) } if err := legacy.Close(); err != nil { t.Fatalf("close legacy db: %v", err) }
d, err := Make(context.Background(), path) if err != nil { t.Fatalf("Make migrated db: %v", err) } defer d.Close() name, labels, ok, err := d.ResolveExecutorToken("hash-1") if err != nil || !ok || name != "exec-1" || !slices.Equal(labels, []string{"arm64"}) { t.Fatalf("ResolveExecutorToken(migrated) = (%q, %v, %v, %v)", name, labels, ok, err) } registrations, err := d.ListExecutorRegistrations() if err != nil || len(registrations) != 1 { t.Fatalf("ListExecutorRegistrations(migrated) = (%+v, %v)", registrations, err) } if registrations[0].CreatedAt != "2026-01-02T03:04:05Z" || registrations[0].ExpiresAt == nil || registrations[0].ExpiresAt.Format(time.RFC3339) != expires { t.Fatalf("migrated credential metadata = %+v, want created and expiry preserved", registrations[0]) } var quarantineReason, quarantinedAt sql.NullString if err := d.QueryRow(` select quarantine_reason, quarantined_at from mill_executors where name = 'exec-1' `).Scan(&quarantineReason, &quarantinedAt); err != nil { t.Fatalf("read migrated compatibility fields: %v", err) } if quarantineReason.Valid || quarantinedAt.Valid { t.Fatalf("migrated compatibility fields were not cleared: %v, %v", quarantineReason, quarantinedAt) } var acked uint64 if err := d.QueryRow(`select acked_seqno from mill_executor_cursors where node_id = 'exec-1' and epoch = 'epoch-1'`).Scan(&acked); err != nil { t.Fatalf("read migrated cursor: %v", err) } if acked != 42 { t.Fatalf("migrated cursor = %d, want 42", acked) }}
func TestCurrentExecutorSchemaClearsLegacyQuarantineState(t *testing.T) { path := filepath.Join(t.TempDir(), "spindle.db") legacy, err := sql.Open("sqlite3", path) if err != nil { t.Fatalf("open legacy db: %v", err) } if _, err := legacy.Exec(` create table mill_executor_tokens ( token_hash text primary key, created_at text not null, expires_at text ); create table mill_executors ( name text primary key, token_hash text not null unique, labels text, quarantine_reason text, quarantined_at text, foreign key (token_hash) references mill_executor_tokens(token_hash) on delete cascade ); create table migrations ( id integer primary key autoincrement, name text unique ); insert into mill_executor_tokens (token_hash, created_at) values ('hash-1', '2026-01-02T03:04:05Z'); insert into mill_executors (name, token_hash, labels, quarantine_reason, quarantined_at) values ('exec-1', 'hash-1', '["arm64"]', 'old quarantine', '2026-01-03T00:00:00Z'); insert into migrations (name) values ('mill-executor-credentials'); `); err != nil { legacy.Close() t.Fatalf("seed current db: %v", err) } if err := legacy.Close(); err != nil { t.Fatalf("close current db: %v", err) }
d, err := Make(context.Background(), path) if err != nil { t.Fatalf("Make migrated db: %v", err) } defer d.Close() name, labels, ok, err := d.ResolveExecutorToken("hash-1") if err != nil || !ok || name != "exec-1" || !slices.Equal(labels, []string{"arm64"}) { t.Fatalf("ResolveExecutorToken(migrated) = (%q, %v, %v, %v)", name, labels, ok, err) } var quarantineReason, quarantinedAt sql.NullString if err := d.QueryRow(` select quarantine_reason, quarantined_at from mill_executors where name = 'exec-1' `).Scan(&quarantineReason, &quarantinedAt); err != nil { t.Fatalf("read migrated compatibility fields: %v", err) } if quarantineReason.Valid || quarantinedAt.Valid { t.Fatalf("migrated compatibility fields were not cleared: %v, %v", quarantineReason, quarantinedAt) }}