Files
at-container-registry/pkg/hold/pds/records_test.go
T

802 lines
24 KiB
Go

package pds
import (
"os"
"path/filepath"
"testing"
_ "github.com/tursodatabase/go-libsql"
)
// Tests for RecordsIndex
// TestNewRecordsIndex tests creating a new records index
func TestNewRecordsIndex(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "records.db")
ri, err := NewRecordsIndex(dbPath)
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
if ri.db == nil {
t.Error("Expected db to be non-nil")
}
// Verify database file was created
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
t.Error("Expected database file to be created")
}
}
// TestNewRecordsIndex_InvalidPath tests error handling for invalid path
func TestNewRecordsIndex_InvalidPath(t *testing.T) {
// Try to create in a non-existent directory
_, err := NewRecordsIndex("/nonexistent/dir/records.db")
if err == nil {
t.Error("Expected error for invalid path")
}
}
// TestRecordsIndex_IndexRecord tests adding records to the index
func TestRecordsIndex_IndexRecord(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Index a record
err = ri.IndexRecord("io.atcr.hold.crew", "abc123", "bafyrei123", "", "", 0)
if err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
// Verify it was indexed
count, err := ri.Count("io.atcr.hold.crew")
if err != nil {
t.Fatalf("Count() error = %v", err)
}
if count != 1 {
t.Errorf("Expected count 1, got %d", count)
}
}
// TestRecordsIndex_IndexRecord_Upsert tests updating an existing record
func TestRecordsIndex_IndexRecord_Upsert(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Index a record
err = ri.IndexRecord("io.atcr.hold.crew", "abc123", "bafyrei123", "", "", 0)
if err != nil {
t.Fatalf("IndexRecord() first call error = %v", err)
}
// Update the same record with new CID
err = ri.IndexRecord("io.atcr.hold.crew", "abc123", "bafyrei456", "", "", 0)
if err != nil {
t.Fatalf("IndexRecord() second call error = %v", err)
}
// Count should still be 1 (upsert, not insert)
count, err := ri.Count("io.atcr.hold.crew")
if err != nil {
t.Fatalf("Count() error = %v", err)
}
if count != 1 {
t.Errorf("Expected count 1 after upsert, got %d", count)
}
// Verify the CID was updated
records, _, err := ri.ListRecords("io.atcr.hold.crew", 10, "", false)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
if len(records) != 1 {
t.Fatalf("Expected 1 record, got %d", len(records))
}
if records[0].Cid != "bafyrei456" {
t.Errorf("Expected CID bafyrei456, got %s", records[0].Cid)
}
}
// TestRecordsIndex_DeleteRecord tests removing a record from the index
func TestRecordsIndex_DeleteRecord(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Index a record
err = ri.IndexRecord("io.atcr.hold.crew", "abc123", "bafyrei123", "", "", 0)
if err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
// Delete it
err = ri.DeleteRecord("io.atcr.hold.crew", "abc123")
if err != nil {
t.Fatalf("DeleteRecord() error = %v", err)
}
// Verify it was deleted
count, err := ri.Count("io.atcr.hold.crew")
if err != nil {
t.Fatalf("Count() error = %v", err)
}
if count != 0 {
t.Errorf("Expected count 0 after delete, got %d", count)
}
}
// TestRecordsIndex_DeleteRecord_NotExists tests deleting a non-existent record
func TestRecordsIndex_DeleteRecord_NotExists(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Delete a record that doesn't exist - should not error
err = ri.DeleteRecord("io.atcr.hold.crew", "nonexistent")
if err != nil {
t.Errorf("DeleteRecord() should not error for non-existent record, got: %v", err)
}
}
// TestRecordsIndex_Close tests clean shutdown
func TestRecordsIndex_Close(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
err = ri.Close()
if err != nil {
t.Errorf("Close() error = %v", err)
}
// Double close should not panic (nil check)
ri.db = nil
err = ri.Close()
if err != nil {
t.Errorf("Close() on nil db error = %v", err)
}
}
// TestRecordsIndex_ListRecords_Empty tests listing an empty collection
func TestRecordsIndex_ListRecords_Empty(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
records, cursor, err := ri.ListRecords("io.atcr.hold.crew", 10, "", false)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
if len(records) != 0 {
t.Errorf("Expected empty records, got %d", len(records))
}
if cursor != "" {
t.Errorf("Expected empty cursor, got %s", cursor)
}
}
// TestRecordsIndex_ListRecords_Basic tests basic listing
func TestRecordsIndex_ListRecords_Basic(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add some records
records := []struct {
rkey string
cid string
}{
{"aaa", "cid1"},
{"bbb", "cid2"},
{"ccc", "cid3"},
}
for _, r := range records {
if err := ri.IndexRecord("io.atcr.hold.crew", r.rkey, r.cid, "", "", 0); err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
}
// List all
result, cursor, err := ri.ListRecords("io.atcr.hold.crew", 10, "", false)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
if len(result) != 3 {
t.Errorf("Expected 3 records, got %d", len(result))
}
if cursor != "" {
t.Errorf("Expected no cursor when all records returned, got %s", cursor)
}
}
// TestRecordsIndex_ListRecords_DefaultOrder tests newest-first ordering (DESC)
func TestRecordsIndex_ListRecords_DefaultOrder(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add records with different rkeys (TIDs are lexicographically ordered by time)
rkeys := []string{"3m3aaaaaaaaa", "3m3bbbbbbbbb", "3m3ccccccccc"}
for _, rkey := range rkeys {
if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey, "", "", 0); err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
}
// List with default order (newest first = DESC)
records, _, err := ri.ListRecords("io.atcr.hold.crew", 10, "", false)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
// Should be in descending order
if len(records) != 3 {
t.Fatalf("Expected 3 records, got %d", len(records))
}
if records[0].Rkey != "3m3ccccccccc" {
t.Errorf("Expected first record rkey=3m3ccccccccc, got %s", records[0].Rkey)
}
if records[1].Rkey != "3m3bbbbbbbbb" {
t.Errorf("Expected second record rkey=3m3bbbbbbbbb, got %s", records[1].Rkey)
}
if records[2].Rkey != "3m3aaaaaaaaa" {
t.Errorf("Expected third record rkey=3m3aaaaaaaaa, got %s", records[2].Rkey)
}
}
// TestRecordsIndex_ListRecords_ReverseOrder tests oldest-first ordering (ASC)
func TestRecordsIndex_ListRecords_ReverseOrder(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add records
rkeys := []string{"3m3aaaaaaaaa", "3m3bbbbbbbbb", "3m3ccccccccc"}
for _, rkey := range rkeys {
if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey, "", "", 0); err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
}
// List with reverse=true (oldest first = ASC)
records, _, err := ri.ListRecords("io.atcr.hold.crew", 10, "", true)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
// Should be in ascending order
if len(records) != 3 {
t.Fatalf("Expected 3 records, got %d", len(records))
}
if records[0].Rkey != "3m3aaaaaaaaa" {
t.Errorf("Expected first record rkey=3m3aaaaaaaaa, got %s", records[0].Rkey)
}
if records[1].Rkey != "3m3bbbbbbbbb" {
t.Errorf("Expected second record rkey=3m3bbbbbbbbb, got %s", records[1].Rkey)
}
if records[2].Rkey != "3m3ccccccccc" {
t.Errorf("Expected third record rkey=3m3ccccccccc, got %s", records[2].Rkey)
}
}
// TestRecordsIndex_ListRecords_Limit tests the limit parameter
func TestRecordsIndex_ListRecords_Limit(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add 5 records
for i := range 5 {
rkey := string(rune('a' + i))
if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey, "", "", 0); err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
}
// List with limit=2
records, cursor, err := ri.ListRecords("io.atcr.hold.crew", 2, "", false)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
if len(records) != 2 {
t.Errorf("Expected 2 records with limit=2, got %d", len(records))
}
if cursor == "" {
t.Error("Expected cursor when more records exist")
}
}
// TestRecordsIndex_ListRecords_Cursor tests pagination with cursor
func TestRecordsIndex_ListRecords_Cursor(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add 5 records
rkeys := []string{"a", "b", "c", "d", "e"}
for _, rkey := range rkeys {
if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey, "", "", 0); err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
}
// First page (default order = DESC, so e, d first)
page1, cursor1, err := ri.ListRecords("io.atcr.hold.crew", 2, "", false)
if err != nil {
t.Fatalf("ListRecords() page 1 error = %v", err)
}
if len(page1) != 2 {
t.Fatalf("Expected 2 records in page 1, got %d", len(page1))
}
if cursor1 == "" {
t.Fatal("Expected cursor after page 1")
}
// Second page using cursor
page2, cursor2, err := ri.ListRecords("io.atcr.hold.crew", 2, cursor1, false)
if err != nil {
t.Fatalf("ListRecords() page 2 error = %v", err)
}
if len(page2) != 2 {
t.Errorf("Expected 2 records in page 2, got %d", len(page2))
}
// Third page
page3, cursor3, err := ri.ListRecords("io.atcr.hold.crew", 2, cursor2, false)
if err != nil {
t.Fatalf("ListRecords() page 3 error = %v", err)
}
if len(page3) != 1 {
t.Errorf("Expected 1 record in page 3, got %d", len(page3))
}
if cursor3 != "" {
t.Errorf("Expected no cursor after last page, got %s", cursor3)
}
// Verify no duplicates across pages
seen := make(map[string]bool)
for _, r := range page1 {
if seen[r.Rkey] {
t.Errorf("Duplicate record: %s", r.Rkey)
}
seen[r.Rkey] = true
}
for _, r := range page2 {
if seen[r.Rkey] {
t.Errorf("Duplicate record: %s", r.Rkey)
}
seen[r.Rkey] = true
}
for _, r := range page3 {
if seen[r.Rkey] {
t.Errorf("Duplicate record: %s", r.Rkey)
}
seen[r.Rkey] = true
}
if len(seen) != 5 {
t.Errorf("Expected 5 unique records, got %d", len(seen))
}
}
// TestRecordsIndex_ListRecords_CursorReverse tests pagination with reverse order
func TestRecordsIndex_ListRecords_CursorReverse(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add 5 records
rkeys := []string{"a", "b", "c", "d", "e"}
for _, rkey := range rkeys {
if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey, "", "", 0); err != nil {
t.Fatalf("IndexRecord() error = %v", err)
}
}
// First page (reverse = ASC, so a, b first)
page1, cursor1, err := ri.ListRecords("io.atcr.hold.crew", 2, "", true)
if err != nil {
t.Fatalf("ListRecords() page 1 error = %v", err)
}
if len(page1) != 2 {
t.Fatalf("Expected 2 records in page 1, got %d", len(page1))
}
if page1[0].Rkey != "a" {
t.Errorf("Expected first record a, got %s", page1[0].Rkey)
}
if page1[1].Rkey != "b" {
t.Errorf("Expected second record b, got %s", page1[1].Rkey)
}
// Second page using cursor
page2, _, err := ri.ListRecords("io.atcr.hold.crew", 2, cursor1, true)
if err != nil {
t.Fatalf("ListRecords() page 2 error = %v", err)
}
if len(page2) != 2 {
t.Errorf("Expected 2 records in page 2, got %d", len(page2))
}
if page2[0].Rkey != "c" {
t.Errorf("Expected first record c, got %s", page2[0].Rkey)
}
}
// TestRecordsIndex_Count tests counting records in a collection
func TestRecordsIndex_Count(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add records to two collections
for i := range 3 {
ri.IndexRecord("io.atcr.hold.crew", string(rune('a'+i)), "cid1", "", "", 0)
}
for i := range 5 {
ri.IndexRecord("io.atcr.hold.captain", string(rune('a'+i)), "cid2", "", "", 0)
}
// Count crew
count, err := ri.Count("io.atcr.hold.crew")
if err != nil {
t.Fatalf("Count() error = %v", err)
}
if count != 3 {
t.Errorf("Expected crew count 3, got %d", count)
}
// Count captain
count, err = ri.Count("io.atcr.hold.captain")
if err != nil {
t.Fatalf("Count() error = %v", err)
}
if count != 5 {
t.Errorf("Expected captain count 5, got %d", count)
}
}
// TestRecordsIndex_Count_Empty tests counting an empty collection
func TestRecordsIndex_Count_Empty(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
count, err := ri.Count("io.atcr.nonexistent")
if err != nil {
t.Fatalf("Count() error = %v", err)
}
if count != 0 {
t.Errorf("Expected count 0 for empty collection, got %d", count)
}
}
// TestRecordsIndex_TotalCount tests total count across all collections
func TestRecordsIndex_TotalCount(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add records to multiple collections
ri.IndexRecord("io.atcr.hold.crew", "a", "cid1", "", "", 0)
ri.IndexRecord("io.atcr.hold.crew", "b", "cid2", "", "", 0)
ri.IndexRecord("io.atcr.hold.captain", "self", "cid3", "", "", 0)
ri.IndexRecord("io.atcr.manifest", "abc123", "cid4", "", "", 0)
count, err := ri.TotalCount()
if err != nil {
t.Fatalf("TotalCount() error = %v", err)
}
if count != 4 {
t.Errorf("Expected total count 4, got %d", count)
}
}
// TestRecordsIndex_BackfillFromRepo_Empty tests backfill with empty repo
func TestRecordsIndex_BackfillFromRepo_Empty(t *testing.T) {
// This test requires a mock repo which is complex to set up
// Skip for now - the integration tests in server_test.go will cover this
t.Skip("Requires mock repo setup - covered by integration tests")
}
// TestRecordsIndex_BackfillFromRepo tests backfill from MST
func TestRecordsIndex_BackfillFromRepo(t *testing.T) {
// This test requires a real repo with MST data
// Skip unit test - covered by integration tests in server_test.go
t.Skip("Requires real repo with MST - covered by integration tests")
}
// TestRecordsIndex_BackfillFromRepo_SkipsWhenSynced tests backfill skip logic
func TestRecordsIndex_BackfillFromRepo_SkipsWhenSynced(t *testing.T) {
// Create a mock scenario where counts match
// This is tested via the count comparison logic
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// The skip logic depends on count comparison in BackfillFromRepo
// which requires a real repo. Skip for now.
t.Skip("Requires mock repo - covered by integration tests")
}
// TestRecordsIndex_MultipleCollections tests isolation between collections
func TestRecordsIndex_MultipleCollections(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add records to different collections with same rkeys
ri.IndexRecord("io.atcr.hold.crew", "abc", "cid-crew", "", "", 0)
ri.IndexRecord("io.atcr.hold.captain", "abc", "cid-captain", "", "", 0)
ri.IndexRecord("io.atcr.manifest", "abc", "cid-manifest", "", "", 0)
// Listing should only return records from requested collection
records, _, err := ri.ListRecords("io.atcr.hold.crew", 10, "", false)
if err != nil {
t.Fatalf("ListRecords() error = %v", err)
}
if len(records) != 1 {
t.Errorf("Expected 1 crew record, got %d", len(records))
}
if records[0].Cid != "cid-crew" {
t.Errorf("Expected cid-crew, got %s", records[0].Cid)
}
// Delete from one collection shouldn't affect others
ri.DeleteRecord("io.atcr.hold.crew", "abc")
count, _ := ri.Count("io.atcr.hold.captain")
if count != 1 {
t.Errorf("Expected captain count 1 after deleting crew, got %d", count)
}
}
// TestRecordsIndex_QuotaForDID tests single-user quota aggregation
func TestRecordsIndex_QuotaForDID(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add layer records for two users, with some duplicate digests
ri.IndexRecord("io.atcr.hold.layer", "r1", "cid1", "did:plc:alice", "sha256:aaa", 100)
ri.IndexRecord("io.atcr.hold.layer", "r2", "cid2", "did:plc:alice", "sha256:bbb", 200)
ri.IndexRecord("io.atcr.hold.layer", "r3", "cid3", "did:plc:alice", "sha256:aaa", 100) // duplicate digest
ri.IndexRecord("io.atcr.hold.layer", "r4", "cid4", "did:plc:bob", "sha256:ccc", 300)
// Alice should have 2 unique blobs, total 300 bytes
uniqueBlobs, totalSize, err := ri.QuotaForDID("io.atcr.hold.layer", "did:plc:alice")
if err != nil {
t.Fatalf("QuotaForDID() error = %v", err)
}
if uniqueBlobs != 2 {
t.Errorf("Expected 2 unique blobs for alice, got %d", uniqueBlobs)
}
if totalSize != 300 {
t.Errorf("Expected total size 300 for alice, got %d", totalSize)
}
// Bob should have 1 unique blob, total 300 bytes
uniqueBlobs, totalSize, err = ri.QuotaForDID("io.atcr.hold.layer", "did:plc:bob")
if err != nil {
t.Fatalf("QuotaForDID() error = %v", err)
}
if uniqueBlobs != 1 {
t.Errorf("Expected 1 unique blob for bob, got %d", uniqueBlobs)
}
if totalSize != 300 {
t.Errorf("Expected total size 300 for bob, got %d", totalSize)
}
// Unknown user should have 0
uniqueBlobs, totalSize, err = ri.QuotaForDID("io.atcr.hold.layer", "did:plc:unknown")
if err != nil {
t.Fatalf("QuotaForDID() error = %v", err)
}
if uniqueBlobs != 0 || totalSize != 0 {
t.Errorf("Expected 0/0 for unknown user, got %d/%d", uniqueBlobs, totalSize)
}
}
// TestRecordsIndex_QuotasByDID tests bulk quota aggregation
func TestRecordsIndex_QuotasByDID(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add layer records for multiple users
ri.IndexRecord("io.atcr.hold.layer", "r1", "cid1", "did:plc:alice", "sha256:aaa", 100)
ri.IndexRecord("io.atcr.hold.layer", "r2", "cid2", "did:plc:alice", "sha256:bbb", 200)
ri.IndexRecord("io.atcr.hold.layer", "r3", "cid3", "did:plc:bob", "sha256:ccc", 500)
// Non-layer record should be excluded
ri.IndexRecord("io.atcr.hold.crew", "r4", "cid4", "did:plc:alice", "", 0)
quotas, err := ri.QuotasByDID("io.atcr.hold.layer")
if err != nil {
t.Fatalf("QuotasByDID() error = %v", err)
}
if len(quotas) != 2 {
t.Fatalf("Expected 2 users in quotas, got %d", len(quotas))
}
alice := quotas["did:plc:alice"]
if alice.UniqueBlobs != 2 {
t.Errorf("Expected 2 unique blobs for alice, got %d", alice.UniqueBlobs)
}
if alice.TotalSize != 300 {
t.Errorf("Expected total size 300 for alice, got %d", alice.TotalSize)
}
bob := quotas["did:plc:bob"]
if bob.UniqueBlobs != 1 {
t.Errorf("Expected 1 unique blob for bob, got %d", bob.UniqueBlobs)
}
if bob.TotalSize != 500 {
t.Errorf("Expected total size 500 for bob, got %d", bob.TotalSize)
}
}
// TestRecordsIndex_QuotasByDID_Empty tests bulk quota with no records
func TestRecordsIndex_QuotasByDID_Empty(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
quotas, err := ri.QuotasByDID("io.atcr.hold.layer")
if err != nil {
t.Fatalf("QuotasByDID() error = %v", err)
}
if len(quotas) != 0 {
t.Errorf("Expected empty quotas map, got %d entries", len(quotas))
}
}
// TestRecordsIndex_QuotaForDID_IgnoresNullDigest tests that records without digest are excluded
func TestRecordsIndex_QuotaForDID_IgnoresNullDigest(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Record with digest+size
ri.IndexRecord("io.atcr.hold.layer", "r1", "cid1", "did:plc:alice", "sha256:aaa", 100)
// Record without digest (e.g. old data before migration)
ri.IndexRecord("io.atcr.hold.layer", "r2", "cid2", "did:plc:alice", "", 0)
uniqueBlobs, totalSize, err := ri.QuotaForDID("io.atcr.hold.layer", "did:plc:alice")
if err != nil {
t.Fatalf("QuotaForDID() error = %v", err)
}
if uniqueBlobs != 1 {
t.Errorf("Expected 1 unique blob (ignoring null digest), got %d", uniqueBlobs)
}
if totalSize != 100 {
t.Errorf("Expected total size 100, got %d", totalSize)
}
}
// TestRecordsIndex_DistinctDIDs tests retrieving unique DIDs from a collection
func TestRecordsIndex_DistinctDIDs(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
// Add layer records for multiple users (some with duplicate DIDs)
ri.IndexRecord("io.atcr.hold.layer", "r1", "cid1", "did:plc:alice", "sha256:aaa", 100)
ri.IndexRecord("io.atcr.hold.layer", "r2", "cid2", "did:plc:alice", "sha256:bbb", 200)
ri.IndexRecord("io.atcr.hold.layer", "r3", "cid3", "did:plc:bob", "sha256:ccc", 500)
// Non-layer record should be excluded
ri.IndexRecord("io.atcr.hold.crew", "r4", "cid4", "did:plc:charlie", "", 0)
dids, err := ri.DistinctDIDs("io.atcr.hold.layer")
if err != nil {
t.Fatalf("DistinctDIDs() error = %v", err)
}
if len(dids) != 2 {
t.Fatalf("Expected 2 distinct DIDs, got %d: %v", len(dids), dids)
}
didSet := make(map[string]bool)
for _, d := range dids {
didSet[d] = true
}
if !didSet["did:plc:alice"] {
t.Error("Expected did:plc:alice in results")
}
if !didSet["did:plc:bob"] {
t.Error("Expected did:plc:bob in results")
}
if didSet["did:plc:charlie"] {
t.Error("did:plc:charlie should not be in layer DIDs")
}
}
// TestRecordsIndex_DistinctDIDs_Empty tests DistinctDIDs with no records
func TestRecordsIndex_DistinctDIDs_Empty(t *testing.T) {
tmpDir := t.TempDir()
ri, err := NewRecordsIndex(filepath.Join(tmpDir, "records.db"))
if err != nil {
t.Fatalf("NewRecordsIndex() error = %v", err)
}
defer ri.Close()
dids, err := ri.DistinctDIDs("io.atcr.hold.layer")
if err != nil {
t.Fatalf("DistinctDIDs() error = %v", err)
}
if len(dids) != 0 {
t.Errorf("Expected empty DIDs slice, got %d entries", len(dids))
}
}