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>
364 lines
9.5 KiB
Go
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)
|
|
}
|
|
}
|