Files
mcr/internal/db/push_test.go
Kyle Isom 61b8c2fcef Fix manifest push 500: use explicit SELECT instead of LastInsertId
SQLite's last_insert_rowid() only updates on actual INSERTs, not
ON CONFLICT DO UPDATE. When pushing a second tag for an existing
manifest digest, the upsert fires the conflict branch (no new row),
so LastInsertId() returns a stale ID from a previous insert. This
caused manifest_blobs and tags to reference the wrong manifest,
producing a 500 on the PUT manifest response.

Replace LastInsertId() with a SELECT id WHERE repository_id AND
digest query within the same transaction.

Security: manifest_blobs and tag foreign keys now always reference
the correct manifest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:35:39 -07:00

364 lines
9.5 KiB
Go

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)
}
}