Files
at-container-registry/pkg/labeler/db_test.go
T

440 lines
12 KiB
Go

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))
}
}