package db import "testing" func TestGetOrCreateRepositoryNew(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } id, err := d.GetOrCreateRepository("newrepo") if err != nil { t.Fatalf("GetOrCreateRepository: %v", err) } if id <= 0 { t.Fatalf("id: got %d, want > 0", id) } // Second call should return the same ID. id2, err := d.GetOrCreateRepository("newrepo") if err != nil { t.Fatalf("GetOrCreateRepository (second): %v", err) } if id2 != id { t.Fatalf("id mismatch: got %d, want %d", id2, id) } } func TestGetOrCreateRepositoryExisting(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } _, err := d.Exec(`INSERT INTO repositories (name) VALUES ('existing')`) if err != nil { t.Fatalf("insert repo: %v", err) } id, err := d.GetOrCreateRepository("existing") if err != nil { t.Fatalf("GetOrCreateRepository: %v", err) } if id <= 0 { t.Fatalf("id: got %d, want > 0", id) } } func TestBlobExists(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } _, err := d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:aaa', 100)`) if err != nil { t.Fatalf("insert blob: %v", err) } exists, err := d.BlobExists("sha256:aaa") if err != nil { t.Fatalf("BlobExists: %v", err) } if !exists { t.Fatal("expected blob to exist") } exists, err = d.BlobExists("sha256:nonexistent") if err != nil { t.Fatalf("BlobExists (nonexistent): %v", err) } if exists { t.Fatal("expected blob to not exist") } } func TestInsertBlob(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } if err := d.InsertBlob("sha256:bbb", 200); err != nil { t.Fatalf("InsertBlob: %v", err) } exists, err := d.BlobExists("sha256:bbb") if err != nil { t.Fatalf("BlobExists: %v", err) } if !exists { t.Fatal("expected blob to exist after insert") } // Insert again — should be a no-op (INSERT OR IGNORE). if err := d.InsertBlob("sha256:bbb", 200); err != nil { t.Fatalf("InsertBlob (dup): %v", err) } } func TestPushManifestByTag(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } // Insert blobs first. if err := d.InsertBlob("sha256:config111", 50); err != nil { t.Fatalf("insert config blob: %v", err) } if err := d.InsertBlob("sha256:layer111", 1000); err != nil { t.Fatalf("insert layer blob: %v", err) } content := []byte(`{"schemaVersion":2}`) params := PushManifestParams{ RepoName: "myrepo", Digest: "sha256:manifest111", MediaType: "application/vnd.oci.image.manifest.v1+json", Content: content, Size: int64(len(content)), Tag: "latest", BlobDigests: []string{"sha256:config111", "sha256:layer111"}, } if err := d.PushManifest(params); err != nil { t.Fatalf("PushManifest: %v", err) } // Verify repository was created. repoID, err := d.GetRepositoryByName("myrepo") if err != nil { t.Fatalf("GetRepositoryByName: %v", err) } if repoID <= 0 { t.Fatalf("repo id: got %d, want > 0", repoID) } // Verify manifest exists. m, err := d.GetManifestByDigest(repoID, "sha256:manifest111") if err != nil { t.Fatalf("GetManifestByDigest: %v", err) } if m.MediaType != "application/vnd.oci.image.manifest.v1+json" { t.Fatalf("media type: got %q", m.MediaType) } if m.Size != int64(len(content)) { t.Fatalf("size: got %d, want %d", m.Size, len(content)) } // Verify tag points to manifest. m2, err := d.GetManifestByTag(repoID, "latest") if err != nil { t.Fatalf("GetManifestByTag: %v", err) } if m2.Digest != "sha256:manifest111" { t.Fatalf("tag digest: got %q", m2.Digest) } // Verify manifest_blobs join table. var mbCount int if err := d.QueryRow(`SELECT COUNT(*) FROM manifest_blobs WHERE manifest_id = ?`, m.ID).Scan(&mbCount); err != nil { t.Fatalf("count manifest_blobs: %v", err) } if mbCount != 2 { t.Fatalf("manifest_blobs count: got %d, want 2", mbCount) } } func TestPushManifestByDigest(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } content := []byte(`{"schemaVersion":2}`) params := PushManifestParams{ RepoName: "myrepo", Digest: "sha256:manifest222", MediaType: "application/vnd.oci.image.manifest.v1+json", Content: content, Size: int64(len(content)), Tag: "", // push by digest — no tag } if err := d.PushManifest(params); err != nil { t.Fatalf("PushManifest: %v", err) } // Verify no tag was created. var tagCount int if err := d.QueryRow(`SELECT COUNT(*) FROM tags`).Scan(&tagCount); err != nil { t.Fatalf("count tags: %v", err) } if tagCount != 0 { t.Fatalf("tag count: got %d, want 0", tagCount) } } func TestPushManifestTagMove(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } // Push first manifest with tag "latest". content1 := []byte(`{"schemaVersion":2,"v":"1"}`) if err := d.PushManifest(PushManifestParams{ RepoName: "myrepo", Digest: "sha256:first", MediaType: "application/vnd.oci.image.manifest.v1+json", Content: content1, Size: int64(len(content1)), Tag: "latest", }); err != nil { t.Fatalf("PushManifest (first): %v", err) } // Push second manifest with same tag "latest" — should atomically move tag. content2 := []byte(`{"schemaVersion":2,"v":"2"}`) if err := d.PushManifest(PushManifestParams{ RepoName: "myrepo", Digest: "sha256:second", MediaType: "application/vnd.oci.image.manifest.v1+json", Content: content2, Size: int64(len(content2)), Tag: "latest", }); err != nil { t.Fatalf("PushManifest (second): %v", err) } repoID, err := d.GetRepositoryByName("myrepo") if err != nil { t.Fatalf("GetRepositoryByName: %v", err) } m, err := d.GetManifestByTag(repoID, "latest") if err != nil { t.Fatalf("GetManifestByTag: %v", err) } if m.Digest != "sha256:second" { t.Fatalf("tag should point to second manifest, got %q", m.Digest) } } func TestPushManifestSecondTagSameDigest(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } // Insert blobs. if err := d.InsertBlob("sha256:cfgA", 50); err != nil { t.Fatalf("InsertBlob cfgA: %v", err) } if err := d.InsertBlob("sha256:lyrA", 1000); err != nil { t.Fatalf("InsertBlob lyrA: %v", err) } contentA := []byte(`{"schemaVersion":2,"image":"A"}`) digestA := "sha256:digestA" // Push app:latest — creates manifest id=1. if err := d.PushManifest(PushManifestParams{ RepoName: "app", Digest: digestA, MediaType: "application/vnd.oci.image.manifest.v1+json", Content: contentA, Size: int64(len(contentA)), Tag: "latest", BlobDigests: []string{"sha256:cfgA", "sha256:lyrA"}, }); err != nil { t.Fatalf("Push app:latest: %v", err) } // Push a different manifest to a different repo to advance the // autoincrement counter, simulating normal production traffic. if err := d.PushManifest(PushManifestParams{ RepoName: "lib", Digest: "sha256:digestB", MediaType: "application/vnd.oci.image.manifest.v1+json", Content: []byte(`{"schemaVersion":2,"image":"B"}`), Size: 30, Tag: "latest", }); err != nil { t.Fatalf("Push lib:latest: %v", err) } // Push app:v1.0.0 with the SAME digest as app:latest. This triggers // the ON CONFLICT DO UPDATE branch. Before the fix, LastInsertId() // returned the wrong manifest ID (lib's manifest), causing the tag // and manifest_blobs to reference the wrong manifest. if err := d.PushManifest(PushManifestParams{ RepoName: "app", Digest: digestA, MediaType: "application/vnd.oci.image.manifest.v1+json", Content: contentA, Size: int64(len(contentA)), Tag: "v1.0.0", BlobDigests: []string{"sha256:cfgA", "sha256:lyrA"}, }); err != nil { t.Fatalf("Push app:v1.0.0: %v", err) } // Verify both tags in "app" point to the same manifest with the correct digest. repoID, err := d.GetRepositoryByName("app") if err != nil { t.Fatalf("GetRepositoryByName: %v", err) } mLatest, err := d.GetManifestByTag(repoID, "latest") if err != nil { t.Fatalf("GetManifestByTag latest: %v", err) } mVersion, err := d.GetManifestByTag(repoID, "v1.0.0") if err != nil { t.Fatalf("GetManifestByTag v1.0.0: %v", err) } if mLatest.ID != mVersion.ID { t.Fatalf("tags point to different manifests: latest=%d, v1.0.0=%d", mLatest.ID, mVersion.ID) } if mLatest.Digest != digestA { t.Fatalf("latest digest: got %q, want %q", mLatest.Digest, digestA) } if mVersion.Digest != digestA { t.Fatalf("v1.0.0 digest: got %q, want %q", mVersion.Digest, digestA) } } func TestPushManifestIdempotent(t *testing.T) { d := openTestDB(t) if err := d.Migrate(); err != nil { t.Fatalf("Migrate: %v", err) } content := []byte(`{"schemaVersion":2}`) params := PushManifestParams{ RepoName: "myrepo", Digest: "sha256:manifest333", MediaType: "application/vnd.oci.image.manifest.v1+json", Content: content, Size: int64(len(content)), Tag: "latest", } // Push twice — should not fail. if err := d.PushManifest(params); err != nil { t.Fatalf("PushManifest (first): %v", err) } if err := d.PushManifest(params); err != nil { t.Fatalf("PushManifest (second): %v", err) } // Verify only one manifest exists. var mCount int if err := d.QueryRow(`SELECT COUNT(*) FROM manifests`).Scan(&mCount); err != nil { t.Fatalf("count manifests: %v", err) } if mCount != 1 { t.Fatalf("manifest count: got %d, want 1", mCount) } }