mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 00:34:16 +00:00
802 lines
24 KiB
Go
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))
|
|
}
|
|
}
|