2026-03-19 21:06:02 +00:00
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func setupMetadataCacheDB(t *testing.T) *DB {
|
|
|
|
|
t.Helper()
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
|
return db
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpsertAndGetMetadataCache(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
entry := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "lodash",
|
|
|
|
|
StoragePath: "_metadata/npm/lodash/metadata",
|
|
|
|
|
ETag: sql.NullString{String: `"abc123"`, Valid: true},
|
|
|
|
|
ContentType: sql.NullString{String: "application/json", Valid: true},
|
2026-07-13 16:24:36 -07:00
|
|
|
ContentDigest: sql.NullString{
|
|
|
|
|
String: "sha256:0123456789abcdef",
|
|
|
|
|
Valid: true,
|
|
|
|
|
},
|
|
|
|
|
Size: sql.NullInt64{Int64: 1024, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
2026-03-19 21:06:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := db.UpsertMetadataCache(entry)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("GetMetadataCache() returned nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if got.Ecosystem != testEcosystemNPM {
|
|
|
|
|
t.Errorf("ecosystem = %q, want %q", got.Ecosystem, testEcosystemNPM)
|
|
|
|
|
}
|
|
|
|
|
if got.Name != "lodash" {
|
|
|
|
|
t.Errorf("name = %q, want %q", got.Name, "lodash")
|
|
|
|
|
}
|
|
|
|
|
if got.StoragePath != "_metadata/npm/lodash/metadata" {
|
|
|
|
|
t.Errorf("storage_path = %q, want %q", got.StoragePath, "_metadata/npm/lodash/metadata")
|
|
|
|
|
}
|
|
|
|
|
if !got.ETag.Valid || got.ETag.String != `"abc123"` {
|
|
|
|
|
t.Errorf("etag = %v, want %q", got.ETag, `"abc123"`)
|
|
|
|
|
}
|
|
|
|
|
if !got.ContentType.Valid || got.ContentType.String != "application/json" {
|
|
|
|
|
t.Errorf("content_type = %v, want %q", got.ContentType, "application/json")
|
|
|
|
|
}
|
2026-07-13 16:24:36 -07:00
|
|
|
if !got.ContentDigest.Valid || got.ContentDigest.String != "sha256:0123456789abcdef" {
|
|
|
|
|
t.Errorf("content_digest = %v, want %q", got.ContentDigest, "sha256:0123456789abcdef")
|
|
|
|
|
}
|
2026-03-19 21:06:02 +00:00
|
|
|
if !got.Size.Valid || got.Size.Int64 != 1024 {
|
|
|
|
|
t.Errorf("size = %v, want 1024", got.Size)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGetMetadataCacheMiss(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "nonexistent")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got != nil {
|
|
|
|
|
t.Errorf("expected nil for cache miss, got %v", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpsertMetadataCacheOverwrite(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
// First insert
|
|
|
|
|
entry1 := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "lodash",
|
|
|
|
|
StoragePath: "_metadata/npm/lodash/metadata",
|
|
|
|
|
ETag: sql.NullString{String: `"v1"`, Valid: true},
|
|
|
|
|
ContentType: sql.NullString{String: "application/json", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 100, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertMetadataCache(entry1); err != nil {
|
|
|
|
|
t.Fatalf("first UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Second insert (same ecosystem+name, different etag and size)
|
|
|
|
|
entry2 := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "lodash",
|
|
|
|
|
StoragePath: "_metadata/npm/lodash/metadata",
|
|
|
|
|
ETag: sql.NullString{String: `"v2"`, Valid: true},
|
|
|
|
|
ContentType: sql.NullString{String: "application/json", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 200, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertMetadataCache(entry2); err != nil {
|
|
|
|
|
t.Fatalf("second UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected entry after overwrite")
|
|
|
|
|
}
|
|
|
|
|
if got.ETag.String != `"v2"` {
|
|
|
|
|
t.Errorf("etag = %q, want %q", got.ETag.String, `"v2"`)
|
|
|
|
|
}
|
|
|
|
|
if got.Size.Int64 != 200 {
|
|
|
|
|
t.Errorf("size = %d, want 200", got.Size.Int64)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpsertMetadataCacheNullableFields(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
entry := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: "pypi",
|
|
|
|
|
Name: "requests",
|
|
|
|
|
StoragePath: "_metadata/pypi/requests/metadata",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.UpsertMetadataCache(entry); err != nil {
|
|
|
|
|
t.Fatalf("UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache("pypi", "requests")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected entry")
|
|
|
|
|
}
|
|
|
|
|
if got.ETag.Valid {
|
|
|
|
|
t.Error("expected null etag")
|
|
|
|
|
}
|
|
|
|
|
if got.ContentType.Valid {
|
|
|
|
|
t.Error("expected null content_type")
|
|
|
|
|
}
|
|
|
|
|
if got.Size.Valid {
|
|
|
|
|
t.Error("expected null size")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMetadataCacheTableCreatedByMigration(t *testing.T) {
|
|
|
|
|
// Create a DB without the metadata_cache table, then migrate
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
// MigrateSchema should create the metadata_cache table
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
has, err := db.HasTable("metadata_cache")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HasTable() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !has {
|
|
|
|
|
t.Error("metadata_cache table should exist after migration")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-13 16:24:36 -07:00
|
|
|
|
|
|
|
|
func TestMetadataCacheContentDigestMigrationPreservesExistingRows(t *testing.T) {
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
if _, err := db.Exec("ALTER TABLE metadata_cache DROP COLUMN content_digest"); err != nil {
|
|
|
|
|
t.Fatalf("dropping content_digest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec("DELETE FROM migrations WHERE name = ?", "006_add_metadata_content_digest"); err != nil {
|
|
|
|
|
t.Fatalf("resetting digest migration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec(`
|
|
|
|
|
INSERT INTO metadata_cache (ecosystem, name, storage_path, content_type, size, fetched_at, created_at, updated_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
`, "oci-manifest", "cache-key", "_metadata/oci-manifest/cache-key/metadata", "application/json", 2, time.Now(), time.Now(), time.Now()); err != nil {
|
|
|
|
|
t.Fatalf("inserting legacy cache row: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
hasDigest, err := db.HasColumn("metadata_cache", "content_digest")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HasColumn() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !hasDigest {
|
|
|
|
|
t.Fatal("metadata_cache.content_digest was not added")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry, err := db.GetMetadataCache("oci-manifest", "cache-key")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if entry == nil || entry.StoragePath != "_metadata/oci-manifest/cache-key/metadata" {
|
|
|
|
|
t.Fatalf("existing metadata cache row was not preserved: %#v", entry)
|
|
|
|
|
}
|
|
|
|
|
if entry.ContentDigest.Valid {
|
|
|
|
|
t.Errorf("legacy content digest = %q, want NULL", entry.ContentDigest.String)
|
|
|
|
|
}
|
|
|
|
|
}
|