mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
440 lines
12 KiB
Go
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))
|
|
}
|
|
}
|