package labeler import ( "database/sql" "os" "path/filepath" "testing" "time" "github.com/bluesky-social/indigo/atproto/atcrypto" ) func newTestKey(t *testing.T) *atcrypto.PrivateKeyK256 { t.Helper() k, err := atcrypto.GeneratePrivateKeyK256() if err != nil { t.Fatalf("generate key: %v", err) } return k } // openTestDB opens a fresh local-only labeler DB and registers cleanup. Returns the // raw *sql.DB so existing tests can keep using it; the wrapper lifecycle is handled // here so tests don't have to know about the embedded-replica machinery. func openTestDB(t *testing.T, path string) *sql.DB { t.Helper() storage, err := OpenDB(path, LibsqlSync{}) if err != nil { t.Fatalf("OpenDB: %v", err) } t.Cleanup(func() { _ = storage.Close() }) return storage.DB } // signAndCreate is a helper that signs the label and inserts it; it returns the row id. func signAndCreate(t *testing.T, db *sql.DB, key *atcrypto.PrivateKeyK256, l *Label) int64 { t.Helper() if err := l.Sign(key); err != nil { t.Fatalf("sign: %v", err) } id, err := CreateLabel(db, l) if err != nil { t.Fatalf("create: %v", err) } return id } func TestOpenDB(t *testing.T) { dir := t.TempDir() dbPath := filepath.Join(dir, "subdir", "test.db") storage, err := OpenDB(dbPath, LibsqlSync{}) if err != nil { t.Fatalf("OpenDB failed: %v", err) } defer storage.Close() if _, err := os.Stat(filepath.Dir(dbPath)); os.IsNotExist(err) { t.Error("expected directory to be created") } var count int if err := storage.DB.QueryRow("SELECT COUNT(*) FROM labels").Scan(&count); err != nil { t.Fatalf("failed to query labels table: %v", err) } if count != 0 { t.Errorf("expected 0 labels, got %d", count) } } func TestCreateLabel(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) key := newTestKey(t) now := time.Now().UTC().Truncate(time.Second) label := &Label{ Src: "did:plc:labeler-1", URI: "at://did:plc:abc/io.atcr.manifest/sha256-123", Val: "!takedown", Cts: now, SubjectDID: "did:plc:abc", SubjectRepo: "myimage", } id := signAndCreate(t, db, key, label) if id <= 0 { t.Errorf("expected positive id, got %d", id) } if len(label.Sig) == 0 { t.Error("expected signature populated by Sign()") } labels, err := GetLabelsSince(db, 0, 10) if err != nil { t.Fatal(err) } if len(labels) != 1 { t.Fatalf("expected 1 label, got %d", len(labels)) } if labels[0].Src != label.Src { t.Errorf("Src = %s, want %s", labels[0].Src, label.Src) } if labels[0].SubjectDID != "did:plc:abc" { t.Errorf("SubjectDID = %s", labels[0].SubjectDID) } if labels[0].SubjectRepo != "myimage" { t.Errorf("SubjectRepo = %s", labels[0].SubjectRepo) } if labels[0].Ver != LabelVersion { t.Errorf("Ver = %d, want %d", labels[0].Ver, LabelVersion) } if len(labels[0].Sig) == 0 { t.Error("expected stored sig to be populated") } } func TestCreateLabel_RejectsUnsigned(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) label := &Label{ Src: "did:plc:labeler-1", URI: "at://did:plc:abc", Val: "!takedown", Cts: time.Now().UTC(), SubjectDID: "did:plc:abc", } if _, err := CreateLabel(db, label); err == nil { t.Fatal("expected CreateLabel to reject an unsigned label") } } func TestSignAndVerify(t *testing.T) { key := newTestKey(t) label := &Label{ Src: "did:plc:labeler-1", URI: "at://did:plc:abc", Val: "!takedown", Cts: time.Now().UTC(), Ver: LabelVersion, } if err := label.Sign(key); err != nil { t.Fatal(err) } pub, err := key.PublicKey() if err != nil { t.Fatal(err) } wire := label.ToLabeling() if err := wire.VerifySignature(pub); err != nil { t.Fatalf("signature did not verify: %v", err) } } func TestListTakedowns(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) key := newTestKey(t) src := "did:plc:labeler-1" now := time.Now().UTC() // Create three takedown events, each with a single summary label, so the // label_count subquery has something to count. var ids []int64 for i, repo := range []string{"repo1", "repo2", "repo3"} { td := &Takedown{ Input: "atcr.io/r/did:plc:abc/" + repo, SubjectDID: "did:plc:abc", SubjectRepo: repo, CreatedAt: now.Add(time.Duration(i) * time.Minute), } id, err := CreateTakedown(db, td) if err != nil { t.Fatalf("CreateTakedown: %v", err) } ids = append(ids, id) signAndCreate(t, db, key, &Label{ Src: src, URI: "at://did:plc:abc/io.atcr.repo/" + repo, Val: "!takedown", Cts: td.CreatedAt, SubjectDID: "did:plc:abc", SubjectRepo: repo, TakedownID: &id, }) } tds, total, err := ListTakedowns(db, TakedownActive, 10, 0) if err != nil { t.Fatal(err) } if total != 3 || len(tds) != 3 { t.Errorf("expected 3 active takedowns, got total=%d returned=%d", total, len(tds)) } for _, td := range tds { if td.LabelCount != 1 { t.Errorf("takedown %d label_count = %d, want 1", td.ID, td.LabelCount) } } // Reverse the middle takedown. if _, err := NegateTakedownLabels(db, key, src, ids[1]); err != nil { t.Fatal(err) } if err := MarkTakedownReversed(db, ids[1], "did:plc:operator", time.Now().UTC()); err != nil { t.Fatal(err) } _, activeTotal, err := ListTakedowns(db, TakedownActive, 10, 0) if err != nil { t.Fatal(err) } if activeTotal != 2 { t.Errorf("expected 2 active takedowns after reversal, got %d", activeTotal) } revs, revTotal, err := ListTakedowns(db, TakedownReversed, 10, 0) if err != nil { t.Fatal(err) } if revTotal != 1 || len(revs) != 1 { t.Errorf("expected 1 reversed takedown, got total=%d returned=%d", revTotal, len(revs)) } if revs[0].ID != ids[1] || revs[0].ReversedAt == nil || revs[0].ReversedBy != "did:plc:operator" { t.Errorf("reversed takedown row has wrong fields: %+v", revs[0]) } } func TestNegateTakedownLabels(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) key := newTestKey(t) src := "did:plc:labeler-1" now := time.Now().UTC() did := "did:plc:abc" tdID, err := CreateTakedown(db, &Takedown{ Input: "atcr.io/r/did:plc:abc/myimage", SubjectDID: did, SubjectRepo: "myimage", CreatedAt: now, }) if err != nil { t.Fatal(err) } uris := []string{ "at://did:plc:abc/io.atcr.manifest/sha256-111", "at://did:plc:abc/io.atcr.manifest/sha256-222", "at://did:plc:abc/io.atcr.tag/myimage-latest", } for _, uri := range uris { signAndCreate(t, db, key, &Label{ Src: src, URI: uri, Val: "!takedown", Cts: now, SubjectDID: did, SubjectRepo: "myimage", TakedownID: &tdID, }) } negs, err := NegateTakedownLabels(db, key, src, tdID) if err != nil { t.Fatal(err) } if len(negs) != len(uris) { t.Errorf("expected %d negation labels, got %d", len(uris), len(negs)) } // Negations must carry the same takedown_id so they're part of the audit trail. all, err := GetLabelsByTakedown(db, tdID) if err != nil { t.Fatal(err) } if len(all) != 2*len(uris) { t.Errorf("expected %d labels (positive + negation) for takedown %d, got %d", 2*len(uris), tdID, len(all)) } var pos, neg int for _, l := range all { if l.TakedownID == nil || *l.TakedownID != tdID { t.Errorf("label %d takedown_id = %v, want %d", l.ID, l.TakedownID, tdID) } if l.Neg { neg++ } else { pos++ } } if pos != len(uris) || neg != len(uris) { t.Errorf("expected pos=%d neg=%d, got pos=%d neg=%d", len(uris), len(uris), pos, neg) } // Calling negate again must be a no-op (no remaining positive labels to flip). negs2, err := NegateTakedownLabels(db, key, src, tdID) if err != nil { t.Fatal(err) } if len(negs2) != 0 { t.Errorf("expected 0 negations on second call, got %d", len(negs2)) } } func TestMarkTakedownReversed_RefusesDoubleReverse(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) id, err := CreateTakedown(db, &Takedown{ Input: "did:plc:abc", SubjectDID: "did:plc:abc", }) if err != nil { t.Fatal(err) } if err := MarkTakedownReversed(db, id, "did:plc:op", time.Now().UTC()); err != nil { t.Fatalf("first reverse: %v", err) } if err := MarkTakedownReversed(db, id, "did:plc:op", time.Now().UTC()); err == nil { t.Error("expected second reverse to fail (already reversed)") } if err := MarkTakedownReversed(db, 9999, "did:plc:op", time.Now().UTC()); err == nil { t.Error("expected reverse on unknown id to fail") } } func TestGetLabelsSince(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) key := newTestKey(t) src := "did:plc:labeler-1" now := time.Now().UTC() for i := range 5 { signAndCreate(t, db, key, &Label{ Src: src, URI: "at://did:plc:abc/io.atcr.manifest/" + string(rune('a'+i)), Val: "!takedown", Cts: now.Add(time.Duration(i) * time.Minute), SubjectDID: "did:plc:abc", SubjectRepo: "repo", }) } labels, err := GetLabelsSince(db, 0, 10) if err != nil { t.Fatal(err) } if len(labels) != 5 { t.Errorf("expected 5 labels, got %d", len(labels)) } cursor := labels[2].ID after, err := GetLabelsSince(db, cursor, 10) if err != nil { t.Fatal(err) } if len(after) != 2 { t.Errorf("expected 2 labels after cursor %d, got %d", cursor, len(after)) } limited, err := GetLabelsSince(db, 0, 2) if err != nil { t.Fatal(err) } if len(limited) != 2 { t.Errorf("expected 2 labels with limit, got %d", len(limited)) } } func TestLatestSeq(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) key := newTestKey(t) if seq, err := LatestSeq(db); err != nil || seq != 0 { t.Fatalf("expected empty seq=0, got %d (err=%v)", seq, err) } id := signAndCreate(t, db, key, &Label{ Src: "did:plc:labeler-1", URI: "at://did:plc:abc", Val: "!takedown", Cts: time.Now().UTC(), SubjectDID: "did:plc:abc", }) seq, err := LatestSeq(db) if err != nil { t.Fatal(err) } if seq != id { t.Errorf("LatestSeq = %d, want %d", seq, id) } } func TestGetLabelsByTakedown(t *testing.T) { dir := t.TempDir() db := openTestDB(t, filepath.Join(dir, "test.db")) key := newTestKey(t) src := "did:plc:labeler-1" now := time.Now().UTC() tdA, err := CreateTakedown(db, &Takedown{Input: "did:plc:abc/repo1", SubjectDID: "did:plc:abc", SubjectRepo: "repo1", CreatedAt: now}) if err != nil { t.Fatal(err) } tdB, err := CreateTakedown(db, &Takedown{Input: "did:plc:def/repo1", SubjectDID: "did:plc:def", SubjectRepo: "repo1", CreatedAt: now}) if err != nil { t.Fatal(err) } signAndCreate(t, db, key, &Label{ Src: src, URI: "at://did:plc:abc/io.atcr.repo/repo1", Val: "!takedown", Cts: now, SubjectDID: "did:plc:abc", SubjectRepo: "repo1", TakedownID: &tdA, }) signAndCreate(t, db, key, &Label{ Src: src, URI: "at://did:plc:abc/io.atcr.manifest/sha256-aaa", Val: "!takedown", Cts: now, SubjectDID: "did:plc:abc", SubjectRepo: "repo1", TakedownID: &tdA, }) signAndCreate(t, db, key, &Label{ Src: src, URI: "at://did:plc:def/io.atcr.repo/repo1", Val: "!takedown", Cts: now, SubjectDID: "did:plc:def", SubjectRepo: "repo1", TakedownID: &tdB, }) labels, err := GetLabelsByTakedown(db, tdA) if err != nil { t.Fatal(err) } if len(labels) != 2 { t.Errorf("takedown A: expected 2 labels, got %d", len(labels)) } for _, l := range labels { if l.TakedownID == nil || *l.TakedownID != tdA { t.Errorf("label %d has wrong takedown_id: %v", l.ID, l.TakedownID) } } labels, err = GetLabelsByTakedown(db, tdB) if err != nil { t.Fatal(err) } if len(labels) != 1 { t.Errorf("takedown B: expected 1 label, got %d", len(labels)) } labels, err = GetLabelsByTakedown(db, 9999) if err != nil { t.Fatal(err) } if len(labels) != 0 { t.Errorf("unknown takedown id: expected 0 labels, got %d", len(labels)) } }