From c966fab53e08321f78719076d6adb1bd188b0b0f Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Tue, 21 Oct 2025 21:06:44 -0500 Subject: [PATCH] add and fix more query tests --- pkg/appview/db/queries.go | 9 +- pkg/appview/db/queries_test.go | 793 +++++++++++++++++++++++++++++++++ 2 files changed, 799 insertions(+), 3 deletions(-) diff --git a/pkg/appview/db/queries.go b/pkg/appview/db/queries.go index 384cbf3..b9d12a2 100644 --- a/pkg/appview/db/queries.go +++ b/pkg/appview/db/queries.go @@ -701,18 +701,18 @@ func GetManifest(db *sql.DB, digest string) (*Manifest, error) { var m Manifest // Use sql.NullString for nullable annotation fields - var title, description, sourceURL, documentationURL, licenses, iconURL sql.NullString + var title, description, sourceURL, documentationURL, licenses, iconURL, readmeURL sql.NullString err := db.QueryRow(` SELECT id, did, repository, digest, hold_endpoint, schema_version, media_type, config_digest, config_size, created_at, - title, description, source_url, documentation_url, licenses, icon_url + title, description, source_url, documentation_url, licenses, icon_url, readme_url FROM manifests WHERE digest = ? `, digest).Scan(&m.ID, &m.DID, &m.Repository, &m.Digest, &m.HoldEndpoint, &m.SchemaVersion, &m.MediaType, &m.ConfigDigest, &m.ConfigSize, &m.CreatedAt, - &title, &description, &sourceURL, &documentationURL, &licenses, &iconURL) + &title, &description, &sourceURL, &documentationURL, &licenses, &iconURL, &readmeURL) if err != nil { return nil, err @@ -737,6 +737,9 @@ func GetManifest(db *sql.DB, digest string) (*Manifest, error) { if iconURL.Valid { m.IconURL = iconURL.String } + if readmeURL.Valid { + m.ReadmeURL = readmeURL.String + } return &m, nil } diff --git a/pkg/appview/db/queries_test.go b/pkg/appview/db/queries_test.go index 0ad76cf..5071dd4 100644 --- a/pkg/appview/db/queries_test.go +++ b/pkg/appview/db/queries_test.go @@ -118,3 +118,796 @@ func TestGetRepositoryMetadata(t *testing.T) { t.Error("Expected all empty strings for manifest with NULL metadata fields") } } + +func TestInsertManifest(t *testing.T) { + // Create in-memory test database + db, err := InitDB(":memory:") + 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 all fields populated + 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(), + Title: "My App", + Description: "A cool application", + SourceURL: "https://github.com/user/myapp", + DocumentationURL: "https://docs.example.com", + Licenses: "MIT", + IconURL: "https://example.com/icon.png", + ReadmeURL: "https://github.com/user/myapp/blob/main/README.md", + } + + id1, err := InsertManifest(db, manifest1) + if err != nil { + t.Fatalf("Failed to insert manifest: %v", err) + } + if id1 == 0 { + t.Error("Expected non-zero manifest ID") + } + + // 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.ID != id1 { + t.Errorf("Expected ID %d, got %d", id1, retrieved.ID) + } + if retrieved.Title != "My App" { + t.Errorf("Expected title 'My App', got '%s'", retrieved.Title) + } + if retrieved.ReadmeURL != "https://github.com/user/myapp/blob/main/README.md" { + t.Errorf("Expected readme_url, got '%s'", retrieved.ReadmeURL) + } + + // 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 == 0 { + t.Error("Expected non-zero manifest ID for minimal manifest") + } + + retrieved2, err := GetManifest(db, manifest2.Digest) + if err != nil { + t.Fatalf("Failed to retrieve minimal manifest: %v", err) + } + if retrieved2.Title != "" { + t.Errorf("Expected empty title for minimal manifest, got '%s'", retrieved2.Title) + } + + // 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(), + Title: "My App v2", + Description: "An updated application", + SourceURL: "https://github.com/user/myapp-v2", + DocumentationURL: "https://v2.docs.example.com", + Licenses: "Apache-2.0", + IconURL: "https://example.com/icon-v2.png", + ReadmeURL: "https://github.com/user/myapp/blob/v2/README.md", + } + + id3, err := InsertManifest(db, manifest1Updated) + if err != nil { + t.Fatalf("Failed to upsert manifest: %v", err) + } + // ID should be the same as the original insert (UPDATE, not INSERT) + if id3 != id1 { + t.Errorf("Expected upsert to return same ID %d, got %d", id1, id3) + } + + // 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.Title != "My App v2" { + t.Errorf("Expected updated title 'My App v2', got '%s'", retrievedUpdated.Title) + } + if retrievedUpdated.HoldEndpoint != "did:web:hold2.example.com" { + t.Errorf("Expected updated hold_endpoint, got '%s'", retrievedUpdated.HoldEndpoint) + } + if retrievedUpdated.ReadmeURL != "https://github.com/user/myapp/blob/v2/README.md" { + t.Errorf("Expected updated readme_url, got '%s'", retrievedUpdated.ReadmeURL) + } + + // 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:") + 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:") + 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(), + Title: "App 1", + }, + { + 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(), + Title: "App 1 v2", + }, + { + 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(), + Title: "App 2", + }, + } + + for _, m := range manifests { + _, err := InsertManifest(db, m) + if err != nil { + t.Fatalf("Failed to insert manifest: %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.Title != "App 1" { + t.Errorf("Expected title 'App 1', got '%s'", retrieved.Title) + } + + // 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:") + 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:") + 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:") + 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) + } + + // 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") + 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{ + ManifestID: manifestID2, + Digest: "sha256:amd64", + Size: 1000, + MediaType: "application/vnd.oci.image.manifest.v1+json", + PlatformOS: "linux", + PlatformArchitecture: "amd64", + ReferenceIndex: 0, + } + ref2 := &ManifestReference{ + ManifestID: 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") + 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 +}