Files
at-container-registry/pkg/appview/db/queries_test.go
T
Evan JarrettandClaude Opus 5 0b212a527f appview: guard the two UI delete paths against cross-repo destruction
Same defect as the OCI path, two more places. The io.atcr.manifest record is
keyed by digest alone, so one record backs every repository of a DID holding
identical content, and both of these deleted it without asking whether another
repository still wants it — then purged the layers on the hold.

DeleteManifestHandler already removes this repository's tags before deleting
the record, so a tag remaining at that point can only belong to another
repository. It now checks IsManifestTaggedAnyRepo there and keeps the shared
record when one does, reporting sharedRecordKept so the caller can tell the
difference between "deleted" and "deliberately left alone".

DeleteUntaggedManifestsHandler is the subtler one. Its digest list comes from
GetAllUntaggedManifestDigests, whose tag join is scoped to one repository
(m.repository = t.repository), so a digest tagged only in a DIFFERENT
repository is reported as untagged and swept. The query is a reasonable
per-repository view and a dangerous delete list; the guard goes at the delete,
not in the query, matching how DeleteTagHandler already works. Skips are
counted separately from failures, because a skip is the guard working and
folding it into "failed" would make a correct run look broken.

Both fail closed. Leaving a manifest behind is recoverable; deleting one
another repository is still serving is not.

The new db test pins both halves of the interaction: that the query really does
report a cross-repo-tagged digest as untagged, so a change there is noticed,
and that IsManifestTaggedAnyRepo answers DID-wide, which is the thing actually
standing between the sweep and another repo's live image.

DeleteTagHandler needed no change — 2580dcd already routed it through
ShouldCascadeDeleteManifest, which is where the correct policy was written down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
2026-08-25 16:34:25 -05:00

2068 lines
66 KiB
Go

package db
import (
"slices"
"testing"
"time"
)
func TestGetRepositoryMetadata(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Insert test user
testUser := &User{
DID: "did:plc:test123",
Handle: "testuser.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
Avatar: "",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
// Test 1: No manifests - should return empty map
metadata, err := GetRepositoryMetadata(db, testUser.DID, "nonexistent")
if err != nil {
t.Fatalf("Expected no error for nonexistent repo, got: %v", err)
}
if len(metadata) != 0 {
t.Errorf("Expected empty map for nonexistent repository, got %d entries", len(metadata))
}
// Test 2: Insert manifest and annotations
_, err = db.Exec(`
INSERT INTO manifests (manifest_key, did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, ManifestKey(testUser.DID, "myapp", "sha256:abc123"),
testUser.DID, "myapp", "sha256:abc123", "did:web:hold.example.com", 2, "application/vnd.oci.image.manifest.v1+json",
time.Now().Add(-2*time.Hour))
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
// Insert annotations separately
err = UpsertRepositoryAnnotations(db, testUser.DID, "myapp", map[string]string{
"org.opencontainers.image.title": "My App",
"org.opencontainers.image.description": "A cool application",
"org.opencontainers.image.source": "https://github.com/user/myapp",
"org.opencontainers.image.documentation": "https://docs.example.com",
"org.opencontainers.image.licenses": "MIT",
"io.atcr.icon": "https://example.com/icon.png",
})
if err != nil {
t.Fatalf("Failed to insert annotations: %v", err)
}
// Test 3: Retrieve metadata
metadata, err = GetRepositoryMetadata(db, testUser.DID, "myapp")
if err != nil {
t.Fatalf("Failed to get repository metadata: %v", err)
}
if metadata["org.opencontainers.image.title"] != "My App" {
t.Errorf("Expected title 'My App', got '%s'", metadata["org.opencontainers.image.title"])
}
if metadata["org.opencontainers.image.description"] != "A cool application" {
t.Errorf("Expected description 'A cool application', got '%s'", metadata["org.opencontainers.image.description"])
}
if metadata["org.opencontainers.image.source"] != "https://github.com/user/myapp" {
t.Errorf("Expected sourceURL 'https://github.com/user/myapp', got '%s'", metadata["org.opencontainers.image.source"])
}
if metadata["org.opencontainers.image.documentation"] != "https://docs.example.com" {
t.Errorf("Expected documentationURL 'https://docs.example.com', got '%s'", metadata["org.opencontainers.image.documentation"])
}
if metadata["org.opencontainers.image.licenses"] != "MIT" {
t.Errorf("Expected licenses 'MIT', got '%s'", metadata["org.opencontainers.image.licenses"])
}
if metadata["io.atcr.icon"] != "https://example.com/icon.png" {
t.Errorf("Expected iconURL 'https://example.com/icon.png', got '%s'", metadata["io.atcr.icon"])
}
// Test 4: Insert newer manifest with different annotations
_, err = db.Exec(`
INSERT INTO manifests (manifest_key, did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, ManifestKey(testUser.DID, "myapp", "sha256:def456"),
testUser.DID, "myapp", "sha256:def456", "did:web:hold.example.com", 2, "application/vnd.oci.image.manifest.v1+json",
time.Now()) // Most recent
if err != nil {
t.Fatalf("Failed to insert newer manifest: %v", err)
}
// Update annotations with new values (simulates latest manifest having different annotations)
err = UpsertRepositoryAnnotations(db, testUser.DID, "myapp", map[string]string{
"org.opencontainers.image.title": "My App v2",
"org.opencontainers.image.description": "An even cooler application",
"org.opencontainers.image.source": "https://github.com/user/myapp-v2",
"org.opencontainers.image.documentation": "https://v2.docs.example.com",
"org.opencontainers.image.licenses": "Apache-2.0",
"io.atcr.icon": "https://example.com/icon-v2.png",
})
if err != nil {
t.Fatalf("Failed to update annotations: %v", err)
}
// Test 5: Should return metadata from most recent manifest
metadata, err = GetRepositoryMetadata(db, testUser.DID, "myapp")
if err != nil {
t.Fatalf("Failed to get repository metadata: %v", err)
}
if metadata["org.opencontainers.image.title"] != "My App v2" {
t.Errorf("Expected title from newest manifest 'My App v2', got '%s'", metadata["org.opencontainers.image.title"])
}
if metadata["org.opencontainers.image.description"] != "An even cooler application" {
t.Errorf("Expected description from newest manifest, got '%s'", metadata["org.opencontainers.image.description"])
}
if metadata["org.opencontainers.image.licenses"] != "Apache-2.0" {
t.Errorf("Expected licenses 'Apache-2.0', got '%s'", metadata["org.opencontainers.image.licenses"])
}
// Test 6: Manifest with NULL metadata fields
_, err = db.Exec(`
INSERT INTO manifests (manifest_key, did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, ManifestKey(testUser.DID, "minimal-app", "sha256:minimal"),
testUser.DID, "minimal-app", "sha256:minimal", "did:web:hold.example.com", 2, "application/vnd.oci.image.manifest.v1+json", time.Now())
if err != nil {
t.Fatalf("Failed to insert minimal manifest: %v", err)
}
// Test 7: Should handle missing annotations gracefully
metadata, err = GetRepositoryMetadata(db, testUser.DID, "minimal-app")
if err != nil {
t.Fatalf("Failed to get repository metadata for minimal app: %v", err)
}
if len(metadata) != 0 {
t.Error("Expected empty map for manifest with no annotations")
}
}
func TestInsertManifest(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Insert test user
testUser := &User{
DID: "did:plc:test123",
Handle: "testuser.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
Avatar: "",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
// Test 1: Insert new manifest with core fields
manifest1 := &Manifest{
DID: testUser.DID,
Repository: "myapp",
Digest: "sha256:abc123",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ConfigDigest: "sha256:config123",
ConfigSize: 1024,
CreatedAt: time.Now(),
}
id1, err := InsertManifest(db, manifest1)
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
if id1 == "" {
t.Error("Expected a manifest key")
}
// Insert annotations separately
annotations := map[string]string{
"org.opencontainers.image.title": "My App",
"org.opencontainers.image.description": "A cool application",
"org.opencontainers.image.source": "https://github.com/user/myapp",
"org.opencontainers.image.documentation": "https://docs.example.com",
"org.opencontainers.image.licenses": "MIT",
"io.atcr.icon": "https://example.com/icon.png",
"io.atcr.readme": "https://github.com/user/myapp/blob/main/README.md",
}
err = UpsertRepositoryAnnotations(db, testUser.DID, "myapp", annotations)
if err != nil {
t.Fatalf("Failed to insert annotations: %v", err)
}
// Verify the manifest was inserted correctly
retrieved, err := GetManifest(db, manifest1.Digest)
if err != nil {
t.Fatalf("Failed to retrieve manifest: %v", err)
}
if retrieved.Key != id1 {
t.Errorf("Expected key %s, got %s", id1, retrieved.Key)
}
// Verify annotations were inserted
retrievedAnnotations, err := GetRepositoryAnnotations(db, testUser.DID, "myapp")
if err != nil {
t.Fatalf("Failed to retrieve annotations: %v", err)
}
if retrievedAnnotations["org.opencontainers.image.title"] != "My App" {
t.Errorf("Expected title 'My App', got '%s'", retrievedAnnotations["org.opencontainers.image.title"])
}
if retrievedAnnotations["io.atcr.readme"] != "https://github.com/user/myapp/blob/main/README.md" {
t.Errorf("Expected readme_url, got '%s'", retrievedAnnotations["io.atcr.readme"])
}
// Test 2: Insert manifest with minimal fields (NULLs for annotations)
manifest2 := &Manifest{
DID: testUser.DID,
Repository: "minimal",
Digest: "sha256:minimal123",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
}
id2, err := InsertManifest(db, manifest2)
if err != nil {
t.Fatalf("Failed to insert minimal manifest: %v", err)
}
if id2 == "" {
t.Error("Expected non-zero manifest ID for minimal manifest")
}
_, err = GetManifest(db, manifest2.Digest)
if err != nil {
t.Fatalf("Failed to retrieve minimal manifest: %v", err)
}
// Verify no annotations exist for minimal manifest
minimalAnnotations, err := GetRepositoryAnnotations(db, testUser.DID, "minimal")
if err != nil {
t.Fatalf("Failed to get minimal annotations: %v", err)
}
if len(minimalAnnotations) != 0 {
t.Errorf("Expected no annotations for minimal manifest, got %d", len(minimalAnnotations))
}
// Test 3: Upsert existing manifest (same DID+repo+digest) - verify UPDATE path
manifest1Updated := &Manifest{
DID: testUser.DID,
Repository: "myapp",
Digest: "sha256:abc123", // Same digest - should trigger UPDATE
HoldEndpoint: "did:web:hold2.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ConfigDigest: "sha256:newconfig",
ConfigSize: 2048,
CreatedAt: time.Now(),
}
id3, err := InsertManifest(db, manifest1Updated)
if err != nil {
t.Fatalf("Failed to upsert manifest: %v", err)
}
// Key should be the same as the original insert (UPDATE, not INSERT)
if id3 != id1 {
t.Errorf("Expected upsert to return the same key %s, got %s", id1, id3)
}
// Update annotations separately
updatedAnnotations := map[string]string{
"org.opencontainers.image.title": "My App v2",
"org.opencontainers.image.description": "An updated application",
"org.opencontainers.image.source": "https://github.com/user/myapp-v2",
"org.opencontainers.image.documentation": "https://v2.docs.example.com",
"org.opencontainers.image.licenses": "Apache-2.0",
"io.atcr.icon": "https://example.com/icon-v2.png",
"io.atcr.readme": "https://github.com/user/myapp/blob/v2/README.md",
}
err = UpsertRepositoryAnnotations(db, testUser.DID, "myapp", updatedAnnotations)
if err != nil {
t.Fatalf("Failed to update annotations: %v", err)
}
// Verify the manifest was updated
retrievedUpdated, err := GetManifest(db, manifest1.Digest)
if err != nil {
t.Fatalf("Failed to retrieve updated manifest: %v", err)
}
if retrievedUpdated.HoldEndpoint != "did:web:hold2.example.com" {
t.Errorf("Expected updated hold_endpoint, got '%s'", retrievedUpdated.HoldEndpoint)
}
// Verify annotations were updated
retrievedUpdatedAnnotations, err := GetRepositoryAnnotations(db, testUser.DID, "myapp")
if err != nil {
t.Fatalf("Failed to retrieve updated annotations: %v", err)
}
if retrievedUpdatedAnnotations["org.opencontainers.image.title"] != "My App v2" {
t.Errorf("Expected updated title 'My App v2', got '%s'", retrievedUpdatedAnnotations["org.opencontainers.image.title"])
}
if retrievedUpdatedAnnotations["io.atcr.readme"] != "https://github.com/user/myapp/blob/v2/README.md" {
t.Errorf("Expected updated readme_url, got '%s'", retrievedUpdatedAnnotations["io.atcr.readme"])
}
// Test 4: Verify count - should have 2 manifests (not 3, because one was upserted)
digests, err := GetManifestDigestsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get manifest digests: %v", err)
}
if len(digests) != 2 {
t.Errorf("Expected 2 manifests after upsert, got %d", len(digests))
}
}
func TestUserManagement(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Test 1: Upsert new user
user1 := &User{
DID: "did:plc:alice123",
Handle: "alice.bsky.social",
PDSEndpoint: "https://bsky.social",
Avatar: "https://example.com/avatar.jpg",
LastSeen: time.Now(),
}
err = UpsertUser(db, user1)
if err != nil {
t.Fatalf("Failed to upsert new user: %v", err)
}
// Test 2: GetUserByDID - found
retrieved, err := GetUserByDID(db, user1.DID)
if err != nil {
t.Fatalf("Failed to get user by DID: %v", err)
}
if retrieved == nil {
t.Fatal("Expected user to be found, got nil")
}
if retrieved.Handle != "alice.bsky.social" {
t.Errorf("Expected handle 'alice.bsky.social', got '%s'", retrieved.Handle)
}
if retrieved.Avatar != "https://example.com/avatar.jpg" {
t.Errorf("Expected avatar URL, got '%s'", retrieved.Avatar)
}
// Test 3: GetUserByHandle - found
retrievedByHandle, err := GetUserByHandle(db, user1.Handle)
if err != nil {
t.Fatalf("Failed to get user by handle: %v", err)
}
if retrievedByHandle == nil {
t.Fatal("Expected user to be found by handle, got nil")
}
if retrievedByHandle.DID != user1.DID {
t.Errorf("Expected DID '%s', got '%s'", user1.DID, retrievedByHandle.DID)
}
// Test 4: GetUserByDID - not found
notFound, err := GetUserByDID(db, "did:plc:nonexistent")
if err != nil {
t.Fatalf("Expected no error for nonexistent user, got: %v", err)
}
if notFound != nil {
t.Error("Expected nil for nonexistent user")
}
// Test 5: GetUserByHandle - not found
notFoundByHandle, err := GetUserByHandle(db, "nonexistent.bsky.social")
if err != nil {
t.Fatalf("Expected no error for nonexistent handle, got: %v", err)
}
if notFoundByHandle != nil {
t.Error("Expected nil for nonexistent handle")
}
// Test 6: Upsert existing user (update)
user1.Handle = "alice-new.bsky.social" // Change handle
user1.Avatar = "" // Remove avatar
user1.LastSeen = time.Now().Add(1 * time.Hour)
err = UpsertUser(db, user1)
if err != nil {
t.Fatalf("Failed to upsert existing user: %v", err)
}
// Verify update
updated, err := GetUserByDID(db, user1.DID)
if err != nil {
t.Fatalf("Failed to get updated user: %v", err)
}
if updated.Handle != "alice-new.bsky.social" {
t.Errorf("Expected updated handle 'alice-new.bsky.social', got '%s'", updated.Handle)
}
if updated.Avatar != "" {
t.Errorf("Expected empty avatar after update, got '%s'", updated.Avatar)
}
// Test 7: User with empty avatar (NULL)
user2 := &User{
DID: "did:plc:bob456",
Handle: "bob.bsky.social",
PDSEndpoint: "https://bsky.social",
Avatar: "", // Empty avatar
LastSeen: time.Now(),
}
err = UpsertUser(db, user2)
if err != nil {
t.Fatalf("Failed to upsert user with empty avatar: %v", err)
}
retrieved2, err := GetUserByDID(db, user2.DID)
if err != nil {
t.Fatalf("Failed to get user with empty avatar: %v", err)
}
if retrieved2.Avatar != "" {
t.Errorf("Expected empty avatar, got '%s'", retrieved2.Avatar)
}
}
func TestManifestOperations(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Setup: Create test user
testUser := &User{
DID: "did:plc:test123",
Handle: "test.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
// Insert test manifests
manifests := []*Manifest{
{
DID: testUser.DID,
Repository: "app1",
Digest: "sha256:aaa",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
},
{
DID: testUser.DID,
Repository: "app1",
Digest: "sha256:bbb",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
},
{
DID: testUser.DID,
Repository: "app2",
Digest: "sha256:ccc",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
},
}
for _, m := range manifests {
_, err := InsertManifest(db, m)
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
}
// Insert annotations for test manifests
err = UpsertRepositoryAnnotations(db, testUser.DID, "app1", map[string]string{
"org.opencontainers.image.title": "App 1",
})
if err != nil {
t.Fatalf("Failed to insert app1 annotations: %v", err)
}
err = UpsertRepositoryAnnotations(db, testUser.DID, "app2", map[string]string{
"org.opencontainers.image.title": "App 2",
})
if err != nil {
t.Fatalf("Failed to insert app2 annotations: %v", err)
}
// Test 1: GetManifest - found
retrieved, err := GetManifest(db, "sha256:aaa")
if err != nil {
t.Fatalf("Failed to get manifest: %v", err)
}
if retrieved.Digest != "sha256:aaa" {
t.Errorf("Expected digest 'sha256:aaa', got '%s'", retrieved.Digest)
}
// Test 2: GetManifest - not found
notFound, err := GetManifest(db, "sha256:nonexistent")
if err == nil {
t.Error("Expected error for nonexistent manifest")
}
if notFound != nil {
t.Error("Expected nil for nonexistent manifest")
}
// Test 3: GetManifestDigestsForDID - multiple manifests
digests, err := GetManifestDigestsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get manifest digests: %v", err)
}
if len(digests) != 3 {
t.Errorf("Expected 3 manifests, got %d", len(digests))
}
// Test 4: GetManifestDigestsForDID - no manifests
noDigests, err := GetManifestDigestsForDID(db, "did:plc:nonexistent")
if err != nil {
t.Fatalf("Expected no error for user with no manifests, got: %v", err)
}
if len(noDigests) != 0 {
t.Errorf("Expected 0 manifests for nonexistent user, got %d", len(noDigests))
}
// Test 5: DeleteManifestsNotInList - keep some, delete others
keepDigests := []string{"sha256:aaa", "sha256:ccc"}
err = DeleteManifestsNotInList(db, testUser.DID, keepDigests)
if err != nil {
t.Fatalf("Failed to delete manifests not in list: %v", err)
}
// Verify only sha256:aaa and sha256:ccc remain
remaining, err := GetManifestDigestsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get remaining manifests: %v", err)
}
if len(remaining) != 2 {
t.Errorf("Expected 2 remaining manifests, got %d", len(remaining))
}
// Verify sha256:bbb was deleted
deleted, err := GetManifest(db, "sha256:bbb")
if err == nil {
t.Error("Expected error for deleted manifest")
}
if deleted != nil {
t.Error("Expected nil for deleted manifest")
}
// Test 6: DeleteManifestsNotInList - empty list (delete all)
err = DeleteManifestsNotInList(db, testUser.DID, []string{})
if err != nil {
t.Fatalf("Failed to delete all manifests: %v", err)
}
allGone, err := GetManifestDigestsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get manifests after delete all: %v", err)
}
if len(allGone) != 0 {
t.Errorf("Expected 0 manifests after delete all, got %d", len(allGone))
}
// Test 7: DeleteManifest - specific deletion (re-insert for this test)
manifest := &Manifest{
DID: testUser.DID,
Repository: "app3",
Digest: "sha256:ddd",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
}
_, err = InsertManifest(db, manifest)
if err != nil {
t.Fatalf("Failed to insert manifest for delete test: %v", err)
}
// Delete by DID+repo+digest
err = DeleteManifest(db, testUser.DID, "app3", "sha256:ddd")
if err != nil {
t.Fatalf("Failed to delete manifest: %v", err)
}
// Verify deletion
afterDelete, err := GetManifest(db, "sha256:ddd")
if err == nil {
t.Error("Expected error for deleted manifest")
}
if afterDelete != nil {
t.Error("Expected nil after deletion")
}
}
func TestIsManifestTagged(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Setup: Create test user
testUser := &User{
DID: "did:plc:test123",
Handle: "test.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
// Insert manifest
manifest := &Manifest{
DID: testUser.DID,
Repository: "myapp",
Digest: "sha256:abc123",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
}
_, err = InsertManifest(db, manifest)
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
// Test 1: Manifest without tags
tagged, err := IsManifestTagged(db, testUser.DID, "myapp", "sha256:abc123")
if err != nil {
t.Fatalf("Failed to check if manifest is tagged: %v", err)
}
if tagged {
t.Error("Expected manifest to not be tagged")
}
// Test 2: Add a tag
tag := &Tag{
DID: testUser.DID,
Repository: "myapp",
Tag: "latest",
Digest: "sha256:abc123",
CreatedAt: time.Now(),
}
err = UpsertTag(db, tag)
if err != nil {
t.Fatalf("Failed to insert tag: %v", err)
}
// Test 3: Manifest with tag
taggedNow, err := IsManifestTagged(db, testUser.DID, "myapp", "sha256:abc123")
if err != nil {
t.Fatalf("Failed to check if manifest is tagged: %v", err)
}
if !taggedNow {
t.Error("Expected manifest to be tagged")
}
}
func TestTagOperations(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Setup: Create test user and manifests
testUser := &User{
DID: "did:plc:test123",
Handle: "test.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
manifest := &Manifest{
DID: testUser.DID,
Repository: "myapp",
Digest: "sha256:abc123",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
}
_, err = InsertManifest(db, manifest)
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
// Test 1: UpsertTag - insert new tag
tag1 := &Tag{
DID: testUser.DID,
Repository: "myapp",
Tag: "latest",
Digest: "sha256:abc123",
CreatedAt: time.Now(),
}
err = UpsertTag(db, tag1)
if err != nil {
t.Fatalf("Failed to upsert tag: %v", err)
}
// Test 2: GetTagsForDID - should have 1 tag
tags, err := GetTagsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get tags: %v", err)
}
if len(tags) != 1 {
t.Errorf("Expected 1 tag, got %d", len(tags))
}
if tags[0].Repository != "myapp" || tags[0].Tag != "latest" {
t.Errorf("Expected myapp:latest, got %s:%s", tags[0].Repository, tags[0].Tag)
}
// Test 3: UpsertTag - update existing tag (point to new digest)
tag1Updated := &Tag{
DID: testUser.DID,
Repository: "myapp",
Tag: "latest", // Same tag
Digest: "sha256:new456",
CreatedAt: time.Now(),
}
err = UpsertTag(db, tag1Updated)
if err != nil {
t.Fatalf("Failed to update tag: %v", err)
}
// Verify update - should still have 1 tag but with new digest
tagsAfterUpdate, err := GetTagsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get tags after update: %v", err)
}
if len(tagsAfterUpdate) != 1 {
t.Errorf("Expected 1 tag after update, got %d", len(tagsAfterUpdate))
}
if tagsAfterUpdate[0].Tag != "latest" {
t.Errorf("Expected tag 'latest', got '%s'", tagsAfterUpdate[0].Tag)
}
// Test 4: Add more tags
tag2 := &Tag{
DID: testUser.DID,
Repository: "myapp",
Tag: "v1.0.0",
Digest: "sha256:abc123",
CreatedAt: time.Now(),
}
tag3 := &Tag{
DID: testUser.DID,
Repository: "otherapp",
Tag: "latest",
Digest: "sha256:xyz789",
CreatedAt: time.Now(),
}
err = UpsertTag(db, tag2)
if err != nil {
t.Fatalf("Failed to insert tag2: %v", err)
}
err = UpsertTag(db, tag3)
if err != nil {
t.Fatalf("Failed to insert tag3: %v", err)
}
// Verify count
allTags, err := GetTagsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get all tags: %v", err)
}
if len(allTags) != 3 {
t.Errorf("Expected 3 tags, got %d", len(allTags))
}
// Test 5: DeleteTagsNotInList - keep some, delete others
keepTags := []struct{ Repository, Tag string }{
{Repository: "myapp", Tag: "latest"},
{Repository: "otherapp", Tag: "latest"},
}
err = DeleteTagsNotInList(db, testUser.DID, keepTags)
if err != nil {
t.Fatalf("Failed to delete tags not in list: %v", err)
}
// Verify v1.0.0 was deleted
remaining, err := GetTagsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get remaining tags: %v", err)
}
if len(remaining) != 2 {
t.Errorf("Expected 2 remaining tags, got %d", len(remaining))
}
// Test 6: DeleteTag - specific deletion
err = DeleteTag(db, testUser.DID, "myapp", "latest")
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
// Verify deletion
afterDelete, err := GetTagsForDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get tags after delete: %v", err)
}
if len(afterDelete) != 1 {
t.Errorf("Expected 1 tag after delete, got %d", len(afterDelete))
}
if afterDelete[0].Repository != "otherapp" {
t.Errorf("Wrong tag remained: %s:%s", afterDelete[0].Repository, afterDelete[0].Tag)
}
// Test 7: GetTagsForDID - no tags
noTags, err := GetTagsForDID(db, "did:plc:nonexistent")
if err != nil {
t.Fatalf("Expected no error for user with no tags, got: %v", err)
}
if len(noTags) != 0 {
t.Errorf("Expected 0 tags for nonexistent user, got %d", len(noTags))
}
}
func TestGetTagsWithPlatforms(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Setup: Create test user
testUser := &User{
DID: "did:plc:test123",
Handle: "test.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
// Register the test hold as public so the hold-access filter allows it
if err := UpsertCaptainRecord(db, &HoldCaptainRecord{
HoldDID: "did:web:hold.example.com",
OwnerDID: "did:plc:holdowner",
Public: true,
}); err != nil {
t.Fatalf("Failed to insert captain record: %v", err)
}
// Test 1: Single-arch manifest (no platform info)
singleArchManifest := &Manifest{
DID: testUser.DID,
Repository: "myapp",
Digest: "sha256:single",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
}
manifestID1, err := InsertManifest(db, singleArchManifest)
if err != nil {
t.Fatalf("Failed to insert single-arch manifest: %v", err)
}
singleTag := &Tag{
DID: testUser.DID,
Repository: "myapp",
Tag: "latest",
Digest: "sha256:single",
CreatedAt: time.Now(),
}
err = UpsertTag(db, singleTag)
if err != nil {
t.Fatalf("Failed to insert single-arch tag: %v", err)
}
tagsWithPlatforms, err := GetTagsWithPlatforms(db, testUser.DID, "myapp", 100, 0, "")
if err != nil {
t.Fatalf("Failed to get tags with platforms: %v", err)
}
if len(tagsWithPlatforms) != 1 {
t.Fatalf("Expected 1 tag, got %d", len(tagsWithPlatforms))
}
if tagsWithPlatforms[0].IsMultiArch {
t.Error("Expected single-arch tag to not be multi-arch")
}
if len(tagsWithPlatforms[0].Platforms) != 0 {
t.Errorf("Expected 0 platforms for single-arch, got %d", len(tagsWithPlatforms[0].Platforms))
}
// Test 2: Multi-arch manifest (manifest list with platform info)
multiArchManifest := &Manifest{
DID: testUser.DID,
Repository: "multiapp",
Digest: "sha256:multi",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.index.v1+json", // Manifest list
CreatedAt: time.Now(),
}
manifestID2, err := InsertManifest(db, multiArchManifest)
if err != nil {
t.Fatalf("Failed to insert multi-arch manifest: %v", err)
}
// Add manifest references with platform info
ref1 := &ManifestReference{
ManifestKey: manifestID2,
Digest: "sha256:amd64",
Size: 1000,
MediaType: "application/vnd.oci.image.manifest.v1+json",
PlatformOS: "linux",
PlatformArchitecture: "amd64",
ReferenceIndex: 0,
}
ref2 := &ManifestReference{
ManifestKey: manifestID2,
Digest: "sha256:arm64",
Size: 1000,
MediaType: "application/vnd.oci.image.manifest.v1+json",
PlatformOS: "linux",
PlatformArchitecture: "arm64",
ReferenceIndex: 1,
}
err = InsertManifestReference(db, ref1)
if err != nil {
t.Fatalf("Failed to insert manifest reference 1: %v", err)
}
err = InsertManifestReference(db, ref2)
if err != nil {
t.Fatalf("Failed to insert manifest reference 2: %v", err)
}
multiTag := &Tag{
DID: testUser.DID,
Repository: "multiapp",
Tag: "latest",
Digest: "sha256:multi",
CreatedAt: time.Now(),
}
err = UpsertTag(db, multiTag)
if err != nil {
t.Fatalf("Failed to insert multi-arch tag: %v", err)
}
multiTagsWithPlatforms, err := GetTagsWithPlatforms(db, testUser.DID, "multiapp", 100, 0, "")
if err != nil {
t.Fatalf("Failed to get multi-arch tags with platforms: %v", err)
}
if len(multiTagsWithPlatforms) != 1 {
t.Fatalf("Expected 1 tag, got %d", len(multiTagsWithPlatforms))
}
if !multiTagsWithPlatforms[0].IsMultiArch {
t.Error("Expected multi-arch tag to be marked as multi-arch")
}
if len(multiTagsWithPlatforms[0].Platforms) != 2 {
t.Errorf("Expected 2 platforms for multi-arch, got %d", len(multiTagsWithPlatforms[0].Platforms))
}
// Verify platform details
platforms := multiTagsWithPlatforms[0].Platforms
if platforms[0].OS != "linux" || platforms[0].Architecture != "amd64" {
t.Errorf("Expected linux/amd64, got %s/%s", platforms[0].OS, platforms[0].Architecture)
}
if platforms[1].OS != "linux" || platforms[1].Architecture != "arm64" {
t.Errorf("Expected linux/arm64, got %s/%s", platforms[1].OS, platforms[1].Architecture)
}
// Don't use manifestID1 since it's not accessed after assignment
_ = manifestID1
}
func TestUserRegistryDomain(t *testing.T) {
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
user := &User{
DID: "did:plc:domainpref",
Handle: "pref.bsky.social",
PDSEndpoint: "https://bsky.social",
LastSeen: time.Now(),
}
if err := UpsertUser(db, user); err != nil {
t.Fatalf("Failed to upsert user: %v", err)
}
// Default is empty string (NULL-safe scan).
got, err := GetUserByDID(db, user.DID)
if err != nil {
t.Fatalf("GetUserByDID failed: %v", err)
}
if got.RegistryDomain != "" {
t.Errorf("Expected empty registry domain by default, got %q", got.RegistryDomain)
}
// Update and confirm it round-trips through both lookups.
if err := UpdateUserRegistryDomain(db, user.DID, "buoy.cr"); err != nil {
t.Fatalf("UpdateUserRegistryDomain failed: %v", err)
}
byDID, err := GetUserByDID(db, user.DID)
if err != nil {
t.Fatalf("GetUserByDID failed: %v", err)
}
if byDID.RegistryDomain != "buoy.cr" {
t.Errorf("GetUserByDID: expected 'buoy.cr', got %q", byDID.RegistryDomain)
}
byHandle, err := GetUserByHandle(db, user.Handle)
if err != nil {
t.Fatalf("GetUserByHandle failed: %v", err)
}
if byHandle.RegistryDomain != "buoy.cr" {
t.Errorf("GetUserByHandle: expected 'buoy.cr', got %q", byHandle.RegistryDomain)
}
// Clearing the preference propagates back to empty.
if err := UpdateUserRegistryDomain(db, user.DID, ""); err != nil {
t.Fatalf("UpdateUserRegistryDomain (clear) failed: %v", err)
}
cleared, err := GetUserByDID(db, user.DID)
if err != nil {
t.Fatalf("GetUserByDID failed: %v", err)
}
if cleared.RegistryDomain != "" {
t.Errorf("Expected empty registry domain after clear, got %q", cleared.RegistryDomain)
}
}
func TestUpdateUserHandle(t *testing.T) {
// Create in-memory test database
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Setup: Create test user
testUser := &User{
DID: "did:plc:alice123",
Handle: "alice.bsky.social",
PDSEndpoint: "https://bsky.social",
Avatar: "https://example.com/avatar.jpg",
LastSeen: time.Now(),
}
err = UpsertUser(db, testUser)
if err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
// Test 1: Update handle for existing user
newHandle := "alice-new.bsky.social"
err = UpdateUserHandle(db, testUser.DID, newHandle)
if err != nil {
t.Fatalf("Failed to update user handle: %v", err)
}
// Verify handle was updated
retrieved, err := GetUserByDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to get user after handle update: %v", err)
}
if retrieved == nil {
t.Fatal("Expected user to be found, got nil")
}
if retrieved.Handle != newHandle {
t.Errorf("Expected handle '%s', got '%s'", newHandle, retrieved.Handle)
}
// Verify other fields unchanged
if retrieved.DID != testUser.DID {
t.Errorf("DID changed unexpectedly: %s -> %s", testUser.DID, retrieved.DID)
}
if retrieved.PDSEndpoint != testUser.PDSEndpoint {
t.Errorf("PDS endpoint changed unexpectedly")
}
if retrieved.Avatar != testUser.Avatar {
t.Errorf("Avatar changed unexpectedly")
}
// Test 2: Update handle for non-existent user (should not error, but no rows affected)
err = UpdateUserHandle(db, "did:plc:nonexistent", "new.handle.social")
if err != nil {
t.Errorf("Expected no error for non-existent user, got: %v", err)
}
// Test 3: Update handle multiple times
handles := []string{"alice1.bsky.social", "alice2.bsky.social", "alice3.bsky.social"}
for _, handle := range handles {
err = UpdateUserHandle(db, testUser.DID, handle)
if err != nil {
t.Fatalf("Failed to update handle to '%s': %v", handle, err)
}
retrieved, err = GetUserByDID(db, testUser.DID)
if err != nil {
t.Fatalf("Failed to retrieve user: %v", err)
}
if retrieved.Handle != handle {
t.Errorf("Expected handle '%s', got '%s'", handle, retrieved.Handle)
}
}
}
// TestEscapeLikePattern tests the SQL LIKE pattern escaping function
func TestEscapeLikePattern(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "plain text",
input: "hello",
expected: "hello",
},
{
name: "with percent wildcard",
input: "hello%world",
expected: "hello\\%world",
},
{
name: "with underscore wildcard",
input: "hello_world",
expected: "hello\\_world",
},
{
name: "with backslash",
input: "hello\\world",
expected: "hello\\\\world",
},
{
name: "with null byte",
input: "test\x00null",
expected: "testnull",
},
{
name: "with control characters",
input: "test\x01\x02control",
expected: "testcontrol",
},
{
name: "keep tabs and newlines",
input: "test\t\n\rwhitespace",
expected: "test\t\n\rwhitespace",
},
{
name: "with leading/trailing spaces",
input: " padded ",
expected: "padded",
},
{
name: "multiple wildcards",
input: "test%_value\\here",
expected: "test\\%\\_value\\\\here",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "only spaces",
input: " ",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := escapeLikePattern(tt.input)
if result != tt.expected {
t.Errorf("escapeLikePattern(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
// TestParseTimestamp tests the timestamp parsing function with multiple formats
func TestParseTimestamp(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
}{
{
name: "RFC3339",
input: "2024-01-01T12:00:00Z",
shouldErr: false,
},
{
name: "RFC3339Nano",
input: "2024-01-01T12:00:00.123456789Z",
shouldErr: false,
},
{
name: "SQLite format",
input: "2024-01-01 12:00:00",
shouldErr: false,
},
{
name: "SQLite with nanos",
input: "2024-01-01 12:00:00.123456789",
shouldErr: false,
},
{
name: "SQLite with timezone",
input: "2024-01-01 12:00:00.123456789-07:00",
shouldErr: false,
},
{
name: "RFC3339 with timezone",
input: "2024-01-01T12:00:00-07:00",
shouldErr: false,
},
{
name: "invalid format",
input: "not-a-date",
shouldErr: true,
},
{
name: "empty string",
input: "",
shouldErr: true,
},
{
name: "partial date",
input: "2024-01-01",
shouldErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseTimestamp(tt.input)
if tt.shouldErr {
if err == nil {
t.Errorf("parseTimestamp(%q) expected error, got nil (result: %v)", tt.input, result)
}
} else {
if err != nil {
t.Errorf("parseTimestamp(%q) unexpected error: %v", tt.input, err)
}
if result.IsZero() {
t.Errorf("parseTimestamp(%q) returned zero time", tt.input)
}
}
})
}
}
func TestDeleteUserData(t *testing.T) {
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Create test user with related data
testUser := &User{
DID: "did:plc:deleteme",
Handle: "deleteme.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: time.Now(),
}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
// Add manifest
manifest := &Manifest{
DID: testUser.DID,
Repository: "myapp",
Digest: "sha256:abc123",
HoldEndpoint: "did:web:hold.example.com",
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
CreatedAt: time.Now(),
}
manifestID, err := InsertManifest(db, manifest)
if err != nil {
t.Fatalf("Failed to insert manifest: %v", err)
}
// Add layer
layer := &Layer{
ManifestKey: manifestID,
LayerIndex: 0,
Digest: "sha256:layer1",
Size: 1000,
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
}
if err := InsertLayer(db, layer); err != nil {
t.Fatalf("Failed to insert layer: %v", err)
}
// Add tag
tag := &Tag{
DID: testUser.DID,
Repository: "myapp",
Tag: "latest",
Digest: "sha256:abc123",
CreatedAt: time.Now(),
}
if err := UpsertTag(db, tag); err != nil {
t.Fatalf("Failed to insert tag: %v", err)
}
// Add annotations
if err := UpsertRepositoryAnnotations(db, testUser.DID, "myapp", map[string]string{
"org.opencontainers.image.title": "My App",
}); err != nil {
t.Fatalf("Failed to insert annotations: %v", err)
}
// Verify data exists
var count int
db.QueryRow(`SELECT COUNT(*) FROM manifests WHERE did = ?`, testUser.DID).Scan(&count)
if count != 1 {
t.Fatalf("Expected 1 manifest, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM tags WHERE did = ?`, testUser.DID).Scan(&count)
if count != 1 {
t.Fatalf("Expected 1 tag, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM layers WHERE manifest_key = ?`, manifestID).Scan(&count)
if count != 1 {
t.Fatalf("Expected 1 layer, got %d", count)
}
// Delete user data
if _, err := DeleteUserData(db, testUser.DID); err != nil {
t.Fatalf("Failed to delete user data: %v", err)
}
// Verify all data was cascade deleted
db.QueryRow(`SELECT COUNT(*) FROM users WHERE did = ?`, testUser.DID).Scan(&count)
if count != 0 {
t.Errorf("Expected 0 users, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM manifests WHERE did = ?`, testUser.DID).Scan(&count)
if count != 0 {
t.Errorf("Expected 0 manifests after cascade delete, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM tags WHERE did = ?`, testUser.DID).Scan(&count)
if count != 0 {
t.Errorf("Expected 0 tags after cascade delete, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM layers WHERE manifest_key = ?`, manifestID).Scan(&count)
if count != 0 {
t.Errorf("Expected 0 layers after cascade delete, got %d", count)
}
// Test idempotency - deleting non-existent user should not error
if deleted, err := DeleteUserData(db, testUser.DID); err != nil {
t.Errorf("Deleting non-existent user should not error, got: %v", err)
} else if deleted {
t.Errorf("Deleting non-existent user should return false, got true")
}
}
func TestIsManifestReferenced(t *testing.T) {
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Insert test user
if err := UpsertUser(db, &User{
DID: "did:plc:test123",
Handle: "testuser.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: time.Now(),
}); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
// Insert a manifest list
_, err = db.Exec(`
INSERT INTO manifests (manifest_key, did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, ManifestKey("did:plc:test123", "myapp", "sha256:indexabc"),
"did:plc:test123", "myapp", "sha256:indexabc", "did:web:hold.example.com", 2,
"application/vnd.oci.image.index.v1+json", time.Now())
if err != nil {
t.Fatalf("Failed to insert manifest list: %v", err)
}
var manifestKey string
db.QueryRow(`SELECT manifest_key FROM manifests WHERE digest = ?`, "sha256:indexabc").Scan(&manifestKey)
// Insert a child manifest reference
_, err = db.Exec(`
INSERT INTO manifest_references (manifest_key, digest, media_type, size, platform_architecture, platform_os, reference_index)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, manifestKey, "sha256:childdef", "application/vnd.oci.image.manifest.v1+json", 1000, "amd64", "linux", 0)
if err != nil {
t.Fatalf("Failed to insert manifest reference: %v", err)
}
// Test 1: child digest should be referenced
referenced, err := IsManifestReferenced(db, "did:plc:test123", "sha256:childdef")
if err != nil {
t.Fatalf("IsManifestReferenced error: %v", err)
}
if !referenced {
t.Error("Expected sha256:childdef to be referenced as a manifest list child")
}
// Test 2: unrelated digest should NOT be referenced
referenced, err = IsManifestReferenced(db, "did:plc:test123", "sha256:unrelated")
if err != nil {
t.Fatalf("IsManifestReferenced error: %v", err)
}
if referenced {
t.Error("Expected sha256:unrelated to NOT be referenced")
}
// Test 3: same digest but different user should NOT be referenced
referenced, err = IsManifestReferenced(db, "did:plc:otheruser", "sha256:childdef")
if err != nil {
t.Fatalf("IsManifestReferenced error: %v", err)
}
if referenced {
t.Error("Expected sha256:childdef to NOT be referenced for different user")
}
}
func TestGetAllUntaggedManifestDigests(t *testing.T) {
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
did := "did:plc:test123"
repo := "myapp"
now := time.Now()
if err := UpsertUser(db, &User{
DID: did,
Handle: "test.bsky.social",
PDSEndpoint: "https://test.pds.example.com",
LastSeen: now,
}); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
indexType := "application/vnd.oci.image.index.v1+json"
manifestType := "application/vnd.oci.image.manifest.v1+json"
hold := "did:web:hold.example.com"
insertManifest := func(t *testing.T, digest, mediaType string) string {
t.Helper()
id, err := InsertManifest(db, &Manifest{
DID: did, Repository: repo, Digest: digest,
HoldEndpoint: hold, SchemaVersion: 2, MediaType: mediaType,
CreatedAt: now,
})
if err != nil {
t.Fatalf("Failed to insert manifest %s: %v", digest, err)
}
return id
}
insertRef := func(t *testing.T, parentKey string, childDigest string, idx int) {
t.Helper()
err := InsertManifestReference(db, &ManifestReference{
ManifestKey: parentKey,
Digest: childDigest,
Size: 1000,
MediaType: manifestType,
PlatformArchitecture: "amd64",
PlatformOS: "linux",
ReferenceIndex: idx,
})
if err != nil {
t.Fatalf("Failed to insert reference: %v", err)
}
}
insertTag := func(t *testing.T, digest, tag string) {
t.Helper()
if err := UpsertTag(db, &Tag{
DID: did, Repository: repo, Tag: tag,
Digest: digest, CreatedAt: now,
}); err != nil {
t.Fatalf("Failed to insert tag: %v", err)
}
}
// Setup scenario:
//
// TAGGED index "sha256:tagged-index" -> tag "v1"
// children: sha256:tagged-child-amd64, sha256:shared-child-arm64
//
// UNTAGGED index "sha256:untagged-index" (no tag)
// children: sha256:untagged-child-amd64, sha256:shared-child-arm64
//
// UNTAGGED orphan single-arch "sha256:orphan-single" (no tag, no parent)
//
// TAGGED single-arch "sha256:tagged-single" -> tag "latest"
// Tagged index + its children
taggedIndexID := insertManifest(t, "sha256:tagged-index", indexType)
insertManifest(t, "sha256:tagged-child-amd64", manifestType)
insertManifest(t, "sha256:shared-child-arm64", manifestType)
insertRef(t, taggedIndexID, "sha256:tagged-child-amd64", 0)
insertRef(t, taggedIndexID, "sha256:shared-child-arm64", 1)
insertTag(t, "sha256:tagged-index", "v1")
// Untagged index + its children
untaggedIndexID := insertManifest(t, "sha256:untagged-index", indexType)
insertManifest(t, "sha256:untagged-child-amd64", manifestType)
// sha256:shared-child-arm64 already inserted, just add the reference
insertRef(t, untaggedIndexID, "sha256:untagged-child-amd64", 0)
insertRef(t, untaggedIndexID, "sha256:shared-child-arm64", 1)
// Orphan single-arch (no parent, no tag)
insertManifest(t, "sha256:orphan-single", manifestType)
// Tagged single-arch
insertManifest(t, "sha256:tagged-single", manifestType)
insertTag(t, "sha256:tagged-single", "latest")
// Run the query
digests, err := GetAllUntaggedManifestDigests(db, did, repo)
if err != nil {
t.Fatalf("GetAllUntaggedManifestDigests error: %v", err)
}
// Build sets for easy checking
digestSet := map[string]bool{}
for _, d := range digests {
digestSet[d] = true
}
// Should include: untagged index, its exclusive child, and the orphan single
if !digestSet["sha256:untagged-index"] {
t.Error("Expected untagged-index to be included")
}
if !digestSet["sha256:untagged-child-amd64"] {
t.Error("Expected untagged-child-amd64 to be included")
}
if !digestSet["sha256:orphan-single"] {
t.Error("Expected orphan-single to be included")
}
// Should NOT include: tagged index, tagged children, shared child (still referenced by tagged index), tagged single
if digestSet["sha256:tagged-index"] {
t.Error("Expected tagged-index to NOT be included")
}
if digestSet["sha256:tagged-child-amd64"] {
t.Error("Expected tagged-child-amd64 to NOT be included")
}
if digestSet["sha256:shared-child-arm64"] {
t.Error("Expected shared-child-arm64 to NOT be included (still referenced by tagged index)")
}
if digestSet["sha256:tagged-single"] {
t.Error("Expected tagged-single to NOT be included")
}
// Verify ordering: children should come before their parent index
childIdx := -1
parentIdx := -1
for i, d := range digests {
if d == "sha256:untagged-child-amd64" {
childIdx = i
}
if d == "sha256:untagged-index" {
parentIdx = i
}
}
if childIdx >= 0 && parentIdx >= 0 && childIdx > parentIdx {
t.Errorf("Expected children before parents: child at index %d, parent at index %d", childIdx, parentIdx)
}
// Verify total count: untagged-child-amd64, orphan-single, untagged-index = 3
if len(digests) != 3 {
t.Errorf("Expected 3 digests, got %d: %v", len(digests), digests)
}
}
// TestGetUserRepositories_HoldAccessFilter verifies that repositories whose
// manifests live on inaccessible holds are hidden from viewers without access.
func TestGetUserRepositories_HoldAccessFilter(t *testing.T) {
db, err := InitDB("file:TestGetUserRepositories_HoldAccessFilter?mode=memory&cache=shared", LibsqlConfig{})
if err != nil {
t.Fatalf("init db: %v", err)
}
defer db.Close()
testUser := &User{DID: "did:plc:alice", Handle: "alice.test", PDSEndpoint: "https://pds.example", LastSeen: time.Now()}
if err := UpsertUser(db, testUser); err != nil {
t.Fatalf("upsert user: %v", err)
}
// Public hold and a private invite-only hold
if err := UpsertCaptainRecord(db, &HoldCaptainRecord{
HoldDID: "did:web:public.example", OwnerDID: "did:plc:holdowner", Public: true,
}); err != nil {
t.Fatalf("seed public captain: %v", err)
}
if err := UpsertCaptainRecord(db, &HoldCaptainRecord{
HoldDID: "did:web:private.example", OwnerDID: "did:plc:holdowner", Public: false, AllowAllCrew: false,
}); err != nil {
t.Fatalf("seed private captain: %v", err)
}
// Two repos: one on the public hold, one on the private hold
if _, err := InsertManifest(db, &Manifest{
DID: testUser.DID, Repository: "publicrepo", Digest: "sha256:pub",
HoldEndpoint: "did:web:public.example", SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json", CreatedAt: time.Now(),
}); err != nil {
t.Fatalf("insert public manifest: %v", err)
}
if _, err := InsertManifest(db, &Manifest{
DID: testUser.DID, Repository: "privaterepo", Digest: "sha256:priv",
HoldEndpoint: "did:web:private.example", SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json", CreatedAt: time.Now(),
}); err != nil {
t.Fatalf("insert private manifest: %v", err)
}
// Anonymous viewer should see only the publicrepo
repos, err := GetUserRepositories(db, testUser.DID, "")
if err != nil {
t.Fatalf("GetUserRepositories anon: %v", err)
}
if len(repos) != 1 || repos[0].Name != "publicrepo" {
t.Errorf("anon viewer: expected [publicrepo], got %v", repos)
}
// Make the private-hold owner a crew member and re-query as them
if err := UpsertCrewMember(db, &CrewMember{
HoldDID: "did:web:private.example", MemberDID: "did:plc:crewdave", Rkey: "rk1",
}); err != nil {
t.Fatalf("upsert crew: %v", err)
}
repos, err = GetUserRepositories(db, testUser.DID, "did:plc:crewdave")
if err != nil {
t.Fatalf("GetUserRepositories crew: %v", err)
}
if len(repos) != 2 {
t.Errorf("crew viewer: expected both repos, got %d: %v", len(repos), repos)
}
}
// TestGetUserRepositories_BulkGrouping verifies that the bulk-fetch
// implementation correctly groups tags, manifests, annotations, and repo-page
// avatars per repository — and that ordering (last_push DESC for repos,
// created_at DESC for tags/manifests within a repo) is preserved.
//
// Regression guard for the previous N+1 implementation, which issued one
// query per repo and per relation.
func TestGetUserRepositories_BulkGrouping(t *testing.T) {
db, err := InitDB("file:TestGetUserRepositories_BulkGrouping?mode=memory&cache=shared", LibsqlConfig{})
if err != nil {
t.Fatalf("init db: %v", err)
}
defer db.Close()
user := &User{DID: "did:plc:owner", Handle: "owner.test", PDSEndpoint: "https://pds.example", LastSeen: time.Now()}
if err := UpsertUser(db, user); err != nil {
t.Fatalf("upsert user: %v", err)
}
if err := UpsertCaptainRecord(db, &HoldCaptainRecord{
HoldDID: "did:web:hold.example", OwnerDID: "did:plc:holdowner", Public: true,
}); err != nil {
t.Fatalf("seed captain: %v", err)
}
now := time.Now().UTC().Truncate(time.Second)
mediaType := "application/vnd.oci.image.manifest.v1+json"
// repoA: two manifests (oldest then newer) and two tags. last_push = now+10s.
manifestA1, err := InsertManifest(db, &Manifest{
DID: user.DID, Repository: "repoA", Digest: "sha256:a1",
HoldEndpoint: "did:web:hold.example", SchemaVersion: 2, MediaType: mediaType,
CreatedAt: now,
})
if err != nil {
t.Fatalf("insert manifest a1: %v", err)
}
manifestA2, err := InsertManifest(db, &Manifest{
DID: user.DID, Repository: "repoA", Digest: "sha256:a2",
HoldEndpoint: "did:web:hold.example", SchemaVersion: 2, MediaType: mediaType,
CreatedAt: now.Add(5 * time.Second),
})
if err != nil {
t.Fatalf("insert manifest a2: %v", err)
}
if err := UpsertTag(db, &Tag{DID: user.DID, Repository: "repoA", Tag: "v1", Digest: "sha256:a1", CreatedAt: now.Add(8 * time.Second)}); err != nil {
t.Fatalf("upsert tag v1: %v", err)
}
if err := UpsertTag(db, &Tag{DID: user.DID, Repository: "repoA", Tag: "v2", Digest: "sha256:a2", CreatedAt: now.Add(10 * time.Second)}); err != nil {
t.Fatalf("upsert tag v2: %v", err)
}
// repoB: one manifest, one tag. last_push = now+1s (older than repoA → repoA sorts first).
if _, err := InsertManifest(db, &Manifest{
DID: user.DID, Repository: "repoB", Digest: "sha256:b1",
HoldEndpoint: "did:web:hold.example", SchemaVersion: 2, MediaType: mediaType,
CreatedAt: now.Add(1 * time.Second),
}); err != nil {
t.Fatalf("insert manifest b1: %v", err)
}
if err := UpsertTag(db, &Tag{DID: user.DID, Repository: "repoB", Tag: "latest", Digest: "sha256:b1", CreatedAt: now.Add(1 * time.Second)}); err != nil {
t.Fatalf("upsert tag b latest: %v", err)
}
// Annotations only on repoA, plus a repo-page avatar on repoB to exercise the icon override.
if err := UpsertRepositoryAnnotations(db, user.DID, "repoA", map[string]string{
"org.opencontainers.image.title": "Repo A Title",
"org.opencontainers.image.description": "alpha",
"io.atcr.icon": "https://example.com/a.png",
}); err != nil {
t.Fatalf("upsert annotations: %v", err)
}
if err := UpsertRepoPage(db, user.DID, "repoB", "", "bafyrepob", false, now, now); err != nil {
t.Fatalf("upsert repo page: %v", err)
}
repos, err := GetUserRepositories(db, user.DID, "")
if err != nil {
t.Fatalf("GetUserRepositories: %v", err)
}
// Order: repoA first (newer last_push), then repoB.
if len(repos) != 2 {
t.Fatalf("expected 2 repos, got %d: %#v", len(repos), repos)
}
if repos[0].Name != "repoA" || repos[1].Name != "repoB" {
t.Fatalf("expected order [repoA, repoB] (last_push DESC), got [%s, %s]", repos[0].Name, repos[1].Name)
}
// repoA grouping
a := repos[0]
if len(a.Tags) != 2 {
t.Errorf("repoA: expected 2 tags, got %d", len(a.Tags))
}
// tags ordered created_at DESC → v2 first
if len(a.Tags) >= 2 && (a.Tags[0].Tag != "v2" || a.Tags[1].Tag != "v1") {
t.Errorf("repoA tags out of order, want [v2, v1] got [%s, %s]", a.Tags[0].Tag, a.Tags[1].Tag)
}
if len(a.Manifests) != 2 {
t.Errorf("repoA: expected 2 manifests, got %d", len(a.Manifests))
}
// manifests ordered created_at DESC → a2 first
if len(a.Manifests) >= 2 && (a.Manifests[0].Key != manifestA2 || a.Manifests[1].Key != manifestA1) {
t.Errorf("repoA manifests out of order, want [a2, a1] got [%s, %s]", a.Manifests[0].Key, a.Manifests[1].Key)
}
if a.Title != "Repo A Title" || a.Description != "alpha" {
t.Errorf("repoA annotations not applied: title=%q desc=%q", a.Title, a.Description)
}
if a.IconURL != "https://example.com/a.png" {
t.Errorf("repoA icon: expected annotation URL, got %q", a.IconURL)
}
// repoB grouping + page-avatar override
b := repos[1]
if len(b.Tags) != 1 || b.Tags[0].Tag != "latest" {
t.Errorf("repoB tags: %#v", b.Tags)
}
if len(b.Manifests) != 1 || b.Manifests[0].Digest != "sha256:b1" {
t.Errorf("repoB manifests: %#v", b.Manifests)
}
if b.IconURL == "" {
t.Errorf("repoB icon should be derived from repo-page avatar CID, got empty")
}
// Cross-repo isolation: tags/manifests for repoB must not leak into repoA and vice versa.
for _, tag := range a.Tags {
if tag.Repository != "repoA" {
t.Errorf("repoA tag has wrong repository: %#v", tag)
}
}
for _, m := range b.Manifests {
if m.Repository != "repoB" {
t.Errorf("repoB manifest has wrong repository: %#v", m)
}
}
}
// TestGetStarredRepoCards verifies the listing of repos starred by a user:
// stars whose target repo no longer has a manifest are silently dropped (the
// "still exists" filter the feature relies on), and results are ordered by
// star creation time DESC.
func TestGetStarredRepoCards(t *testing.T) {
db, err := InitDB("file:TestGetStarredRepoCards?mode=memory&cache=shared", LibsqlConfig{})
if err != nil {
t.Fatalf("init db: %v", err)
}
defer db.Close()
now := time.Now().UTC().Truncate(time.Second)
mediaType := "application/vnd.oci.image.manifest.v1+json"
starrer := &User{DID: "did:plc:starrer", Handle: "starrer.test", PDSEndpoint: "https://pds.example", LastSeen: now}
if err := UpsertUser(db, starrer); err != nil {
t.Fatalf("upsert starrer: %v", err)
}
owner := &User{DID: "did:plc:owner", Handle: "owner.test", PDSEndpoint: "https://pds.example", LastSeen: now}
if err := UpsertUser(db, owner); err != nil {
t.Fatalf("upsert owner: %v", err)
}
// Owner of a deleted repo (still has a users row, just no manifests).
ghost := &User{DID: "did:plc:ghost", Handle: "ghost.test", PDSEndpoint: "https://pds.example", LastSeen: now}
if err := UpsertUser(db, ghost); err != nil {
t.Fatalf("upsert ghost: %v", err)
}
if err := UpsertCaptainRecord(db, &HoldCaptainRecord{
HoldDID: "did:web:hold.example", OwnerDID: "did:plc:holdowner", Public: true,
}); err != nil {
t.Fatalf("seed captain: %v", err)
}
// Two existing repos owned by `owner`.
if _, err := InsertManifest(db, &Manifest{
DID: owner.DID, Repository: "repo-old", Digest: "sha256:old",
HoldEndpoint: "did:web:hold.example", SchemaVersion: 2, MediaType: mediaType,
CreatedAt: now,
}); err != nil {
t.Fatalf("insert old manifest: %v", err)
}
if _, err := InsertManifest(db, &Manifest{
DID: owner.DID, Repository: "repo-new", Digest: "sha256:new",
HoldEndpoint: "did:web:hold.example", SchemaVersion: 2, MediaType: mediaType,
CreatedAt: now.Add(5 * time.Second),
}); err != nil {
t.Fatalf("insert new manifest: %v", err)
}
// Three stars: two for existing repos, one for a deleted repo.
if err := UpsertStar(db, starrer.DID, owner.DID, "repo-old", now); err != nil {
t.Fatalf("upsert star repo-old: %v", err)
}
if err := UpsertStar(db, starrer.DID, owner.DID, "repo-new", now.Add(10*time.Second)); err != nil {
t.Fatalf("upsert star repo-new: %v", err)
}
if err := UpsertStar(db, starrer.DID, ghost.DID, "deleted-repo", now.Add(20*time.Second)); err != nil {
t.Fatalf("upsert star deleted-repo: %v", err)
}
cards, err := GetStarredRepoCards(db, starrer.DID, starrer.DID)
if err != nil {
t.Fatalf("GetStarredRepoCards: %v", err)
}
if len(cards) != 2 {
t.Fatalf("expected 2 cards (deleted repo dropped), got %d: %+v", len(cards), cards)
}
// Newest star first.
if cards[0].Repository != "repo-new" || cards[1].Repository != "repo-old" {
t.Errorf("expected order [repo-new, repo-old] (newest star first), got [%s, %s]", cards[0].Repository, cards[1].Repository)
}
// IsStarred should reflect the viewer's perspective. Viewer == starrer here
// so every returned row is starred-by-viewer.
for _, c := range cards {
if !c.IsStarred {
t.Errorf("card %s/%s expected IsStarred=true for self-viewer", c.OwnerHandle, c.Repository)
}
}
}
// TestGetUserRepositories_Empty verifies the bulk-fetch path short-circuits
// cleanly when the summary query returns no rows (no extra queries issued,
// nil slice returned).
func TestGetUserRepositories_Empty(t *testing.T) {
db, err := InitDB("file:TestGetUserRepositories_Empty?mode=memory&cache=shared", LibsqlConfig{})
if err != nil {
t.Fatalf("init db: %v", err)
}
defer db.Close()
user := &User{DID: "did:plc:nobody", Handle: "nobody.test", PDSEndpoint: "https://pds.example", LastSeen: time.Now()}
if err := UpsertUser(db, user); err != nil {
t.Fatalf("upsert user: %v", err)
}
repos, err := GetUserRepositories(db, user.DID, "")
if err != nil {
t.Fatalf("GetUserRepositories empty: %v", err)
}
if repos != nil {
t.Errorf("expected nil slice for user with no repos, got %#v", repos)
}
}
// TestGetRepoCards_NullLastPushStillSortsByCreatedAt is a regression test for
// the "What's New" NULL-poison bug: a repo whose repository_stats row exists
// but has last_push = NULL (e.g. created by a pull when the push-notify never
// recorded a push) must still appear and sort by its manifest created_at.
//
// SQLite's scalar 2-arg max() returns NULL if any argument is NULL, so the old
// `MAX(rs.last_push, m.created_at)` produced a NULL sort key for such repos,
// dropping them to the bottom (and out of the limited "What's New" list) and
// rendering a blank "last updated" timestamp. The fix wraps last_push in
// COALESCE so it falls back to created_at.
func TestGetRepoCards_NullLastPushStillSortsByCreatedAt(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
// A public hold so the manifests are accessible to anonymous viewers.
if _, err := d.Exec(`
INSERT INTO hold_captain_records (hold_did, owner_did, public, allow_all_crew)
VALUES ('did:web:hold', 'did:plc:alice', 1, 0)
`); err != nil {
t.Fatalf("seed captain: %v", err)
}
now := time.Now()
old := now.Add(-240 * time.Hour) // 10 days ago
// "fresh": recent push, but its stats row has last_push = NULL (pulled,
// never push-recorded). "stale": older, with a real last_push.
if err := BatchInsertManifests(d, []Manifest{
{DID: "did:plc:alice", Repository: "fresh", Digest: "sha256:fresh", HoldEndpoint: "did:web:hold", SchemaVersion: 2, MediaType: "application/vnd.oci.image.manifest.v1+json", ArtifactType: "container-image", CreatedAt: now},
{DID: "did:plc:alice", Repository: "stale", Digest: "sha256:stale", HoldEndpoint: "did:web:hold", SchemaVersion: 2, MediaType: "application/vnd.oci.image.manifest.v1+json", ArtifactType: "container-image", CreatedAt: old},
}); err != nil {
t.Fatalf("insert manifests: %v", err)
}
// fresh: pull-only stats row, last_push explicitly NULL (the bug trigger).
if _, err := d.Exec(`
INSERT INTO repository_stats (did, repository, pull_count, last_pull, push_count, last_push)
VALUES ('did:plc:alice', 'fresh', 2, ?, 0, NULL)
`, now.Format(time.RFC3339)); err != nil {
t.Fatalf("seed fresh stats: %v", err)
}
// stale: has a genuine last_push, older than fresh's created_at.
if _, err := d.Exec(`
INSERT INTO repository_stats (did, repository, pull_count, last_pull, push_count, last_push)
VALUES ('did:plc:alice', 'stale', 0, NULL, 1, ?)
`, old.Format(time.RFC3339)); err != nil {
t.Fatalf("seed stale stats: %v", err)
}
cards, err := GetRepoCards(d, 18, "", SortByLastUpdate)
if err != nil {
t.Fatalf("GetRepoCards: %v", err)
}
if len(cards) != 2 {
t.Fatalf("expected 2 cards, got %d: %#v", len(cards), cards)
}
// fresh (recent created_at) must sort above stale (old last_push), despite
// fresh's last_push being NULL.
if cards[0].Repository != "fresh" {
t.Errorf("expected 'fresh' first (sorted by created_at), got order %q, %q", cards[0].Repository, cards[1].Repository)
}
// And its "last updated" must be populated from created_at, not blank.
var fresh *RepoCardData
for i := range cards {
if cards[i].Repository == "fresh" {
fresh = &cards[i]
}
}
if fresh == nil {
t.Fatal("'fresh' repo missing from What's New")
}
if fresh.LastUpdated.IsZero() {
t.Error("expected 'fresh' LastUpdated to fall back to created_at, got zero time")
}
}
// TestGetAllUntaggedManifestDigests_TagInOtherRepoIsNotUntagged pins the
// interaction that makes the delete-untagged path safe.
//
// GetAllUntaggedManifestDigests joins tags scoped to one repository
// (m.repository = t.repository), so a digest tagged only in a DIFFERENT
// repository of the same DID is reported as untagged here. That is fine as a
// per-repository view, and dangerous as a delete list: the io.atcr.manifest
// record is keyed by digest alone, so one record backs every repository holding
// identical content, and deleting on this basis strips the record out from
// under the other repo and purges the shared layers on the hold.
//
// The handler therefore gates each digest on IsManifestTaggedAnyRepo, which is
// DID-wide. Both halves are asserted: the query's repo-scoped answer, so a
// future change to it is noticed, and the guard's DID-wide answer, which is the
// thing actually standing between a sweep and another repo's live image.
func TestGetAllUntaggedManifestDigests_TagInOtherRepoIsNotUntagged(t *testing.T) {
db, err := InitDB(":memory:", LibsqlConfig{})
if err != nil {
t.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
did := "did:plc:shared"
shared := "sha256:sharedcontent"
now := time.Now()
manifestType := "application/vnd.oci.image.manifest.v1+json"
if err := UpsertUser(db, &User{
DID: did, Handle: "shared.test",
PDSEndpoint: "https://pds.example.com", LastSeen: now,
}); err != nil {
t.Fatalf("Failed to insert user: %v", err)
}
// Identical content in two repositories — what `crane copy` produces, and
// what any two images built on the same base produce by accident.
for _, repo := range []string{"app-a", "app-b"} {
if _, err := InsertManifest(db, &Manifest{
DID: did, Repository: repo, Digest: shared,
HoldEndpoint: "did:web:hold.example.com", SchemaVersion: 2,
MediaType: manifestType, CreatedAt: now,
}); err != nil {
t.Fatalf("Failed to insert manifest for %s: %v", repo, err)
}
}
// Tagged in app-b only. app-a holds the same content, untagged.
if err := UpsertTag(db, &Tag{
DID: did, Repository: "app-b", Tag: "v1",
Digest: shared, CreatedAt: now,
}); err != nil {
t.Fatalf("Failed to insert tag: %v", err)
}
untagged, err := GetAllUntaggedManifestDigests(db, did, "app-a")
if err != nil {
t.Fatalf("GetAllUntaggedManifestDigests: %v", err)
}
if !slices.Contains(untagged, shared) {
t.Errorf("expected the repo-scoped view to report %s as untagged in app-a, got %v; "+
"if this query became DID-wide the handler guard is now redundant, not wrong", shared, untagged)
}
tagged, err := IsManifestTaggedAnyRepo(db, did, shared)
if err != nil {
t.Fatalf("IsManifestTaggedAnyRepo: %v", err)
}
if !tagged {
t.Error("IsManifestTaggedAnyRepo said the shared digest is untagged while app-b still tags it; " +
"the delete-untagged sweep would remove the record and purge app-b's layers")
}
}