package store import ( "context" "errors" "testing" "time" ) // TestDeleteHostCascades verifies that DeleteHost removes the host // row and that every dependent table (schedules, jobs, source groups, // host_credentials) is wiped via the FK cascade declared in the // migrations. We also verify the agent bearer is no longer resolvable // — a re-installed agent must come back through pending-host accept. func TestDeleteHostCascades(t *testing.T) { t.Parallel() s := openTestStore(t) ctx := context.Background() hostID := makeSchedHost(t, s) gid := makeGroup(t, s, hostID, "default", "01HDELGRP000000000000001") // One job, one schedule, one credential row — enough to prove the // cascade reaches every dependent table we care about. if err := s.CreateJob(ctx, Job{ ID: "j-del-1", HostID: hostID, Kind: "backup", ActorKind: "system", CreatedAt: time.Now().UTC(), }); err != nil { t.Fatalf("create job: %v", err) } sched := &Schedule{ ID: "01HDELSCHED00000000000001", HostID: hostID, CronExpr: "0 3 * * *", Enabled: true, SourceGroupIDs: []string{gid}, } if err := s.CreateSchedule(ctx, sched); err != nil { t.Fatalf("create schedule: %v", err) } if err := s.SetHostCredentials(ctx, hostID, CredKindRepo, "ciphertext"); err != nil { t.Fatalf("set creds: %v", err) } // Sanity: agent bearer resolves before deletion. if _, err := s.LookupHostByAgentToken(ctx, "tokenhash"); err != nil { t.Fatalf("pre-delete bearer lookup: %v", err) } if err := s.DeleteHost(ctx, hostID); err != nil { t.Fatalf("DeleteHost: %v", err) } if _, err := s.GetHost(ctx, hostID); !errors.Is(err, ErrNotFound) { t.Errorf("GetHost after delete: want ErrNotFound, got %v", err) } if _, err := s.LookupHostByAgentToken(ctx, "tokenhash"); !errors.Is(err, ErrNotFound) { t.Errorf("bearer lookup after delete: want ErrNotFound, got %v", err) } // Cascade smoke-tests via raw counts. We don't own a public // "list jobs by host" path that filters by host, so go to the DB // directly with the same connection used by the store helpers. for _, q := range []struct { label string sql string }{ {"schedules", "SELECT count(*) FROM schedules WHERE host_id = ?"}, {"jobs", "SELECT count(*) FROM jobs WHERE host_id = ?"}, {"source_groups", "SELECT count(*) FROM source_groups WHERE host_id = ?"}, {"host_credentials", "SELECT count(*) FROM host_credentials WHERE host_id = ?"}, {"schedule_source_groups", "SELECT count(*) FROM schedule_source_groups WHERE schedule_id = ?"}, } { var n int key := hostID if q.label == "schedule_source_groups" { key = "01HDELSCHED00000000000001" } if err := s.db.QueryRowContext(ctx, q.sql, key).Scan(&n); err != nil { t.Fatalf("count %s: %v", q.label, err) } if n != 0 { t.Errorf("cascade left %d rows in %s", n, q.label) } } } // TestDeleteHostNotFound: a delete against a missing id surfaces // ErrNotFound so the HTTP layer can 404 instead of 200-ing a no-op. func TestDeleteHostNotFound(t *testing.T) { t.Parallel() s := openTestStore(t) if err := s.DeleteHost(context.Background(), "01HNOTAHOST00000000000000"); !errors.Is(err, ErrNotFound) { t.Errorf("missing id: want ErrNotFound, got %v", err) } }