2026-01-20 21:52:44 +00:00
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestCreateAndOpen(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
version, err := db.SchemaVersion()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SchemaVersion failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if version != SchemaVersion {
|
|
|
|
|
t.Errorf("expected schema version %d, got %d", SchemaVersion, version)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.Close(); err != nil {
|
|
|
|
|
t.Fatalf("Close failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db, err = Open(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Open failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
version, err = db.SchemaVersion()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SchemaVersion after reopen failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if version != SchemaVersion {
|
|
|
|
|
t.Errorf("expected schema version %d after reopen, got %d", SchemaVersion, version)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOpenOrCreate(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
db, err := OpenOrCreate(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenOrCreate (create) failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.Close(); err != nil {
|
|
|
|
|
t.Fatalf("Close failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db, err = OpenOrCreate(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("OpenOrCreate (open) failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPackageCRUD(t *testing.T) {
|
2026-01-29 16:06:56 +00:00
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/lodash",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "lodash",
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://registry.npmjs.org/lodash", Valid: true},
|
2026-01-29 16:06:56 +00:00
|
|
|
Description: sql.NullString{String: "Lodash library", Valid: true},
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
err := db.UpsertPackage(pkg)
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertPackage failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
got, err := db.GetPackageByPURL("pkg:npm/lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetPackageByPURL failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected package, got nil")
|
|
|
|
|
}
|
|
|
|
|
if got.Name != "lodash" {
|
|
|
|
|
t.Errorf("expected name lodash, got %s", got.Name)
|
|
|
|
|
}
|
|
|
|
|
if got.Description.String != "Lodash library" {
|
|
|
|
|
t.Errorf("expected description 'Lodash library', got %s", got.Description.String)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
got, err = db.GetPackageByEcosystemName("npm", "lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetPackageByEcosystemName failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected package, got nil")
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
pkg.Description = sql.NullString{String: "Updated description", Valid: true}
|
2026-01-29 16:44:01 +00:00
|
|
|
err = db.UpsertPackage(pkg)
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertPackage (update) failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
got, err = db.GetPackageByPURL("pkg:npm/lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetPackageByPURL after update failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got.Description.String != "Updated description" {
|
|
|
|
|
t.Errorf("expected updated description, got %s", got.Description.String)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestVersionCRUD(t *testing.T) {
|
2026-01-29 16:06:56 +00:00
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/lodash",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "lodash",
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://registry.npmjs.org/lodash", Valid: true},
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
err := db.UpsertPackage(pkg)
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertPackage failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
v := &Version{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: "pkg:npm/lodash@4.17.21",
|
|
|
|
|
PackagePURL: "pkg:npm/lodash",
|
|
|
|
|
Integrity: sql.NullString{String: "sha512-abc123", Valid: true},
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
err = db.UpsertVersion(v)
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertVersion failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
got, err := db.GetVersionByPURL("pkg:npm/lodash@4.17.21")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetVersionByPURL failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected version, got nil")
|
|
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
if got.Version() != "4.17.21" {
|
|
|
|
|
t.Errorf("expected version 4.17.21, got %s", got.Version())
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
versions, err := db.GetVersionsByPackagePURL("pkg:npm/lodash")
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
2026-01-29 16:44:01 +00:00
|
|
|
t.Fatalf("GetVersionsByPackagePURL failed: %v", err)
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
|
|
|
|
if len(versions) != 1 {
|
|
|
|
|
t.Errorf("expected 1 version, got %d", len(versions))
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestArtifactCRUD(t *testing.T) {
|
2026-01-29 16:06:56 +00:00
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/lodash",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "lodash",
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://registry.npmjs.org/lodash", Valid: true},
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertPackage(pkg)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
versionPURL := "pkg:npm/lodash@4.17.21"
|
2026-01-29 16:06:56 +00:00
|
|
|
v := &Version{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: versionPURL,
|
|
|
|
|
PackagePURL: "pkg:npm/lodash",
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertVersion(v)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
a := &Artifact{
|
2026-01-29 16:44:01 +00:00
|
|
|
VersionPURL: versionPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Filename: "lodash-4.17.21.tgz",
|
|
|
|
|
UpstreamURL: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
err := db.UpsertArtifact(a)
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertArtifact failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
got, err := db.GetArtifact(versionPURL, "lodash-4.17.21.tgz")
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetArtifact failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected artifact, got nil")
|
|
|
|
|
}
|
|
|
|
|
if got.IsCached() {
|
|
|
|
|
t.Error("expected artifact to not be cached yet")
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
err = db.MarkArtifactCached(versionPURL, "lodash-4.17.21.tgz", "/cache/npm/lodash-4.17.21.tgz", "sha256-abc", 12345, "application/gzip")
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("MarkArtifactCached failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
got, err = db.GetArtifact(versionPURL, "lodash-4.17.21.tgz")
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetArtifact after cache failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !got.IsCached() {
|
|
|
|
|
t.Error("expected artifact to be cached")
|
|
|
|
|
}
|
|
|
|
|
if got.Size.Int64 != 12345 {
|
|
|
|
|
t.Errorf("expected size 12345, got %d", got.Size.Int64)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
got, err = db.GetArtifactByPath("/cache/npm/lodash-4.17.21.tgz")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetArtifactByPath failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected artifact by path, got nil")
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
err = db.RecordArtifactHit(versionPURL, "lodash-4.17.21.tgz")
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RecordArtifactHit failed: %v", err)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
got, err = db.GetArtifact(versionPURL, "lodash-4.17.21.tgz")
|
2026-01-29 16:06:56 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetArtifact after hit failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got.HitCount != 1 {
|
|
|
|
|
t.Errorf("expected hit count 1, got %d", got.HitCount)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCacheManagement(t *testing.T) {
|
2026-01-29 16:06:56 +00:00
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/test",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "test",
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://registry.npmjs.org/test", Valid: true},
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertPackage(pkg)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
for i := 1; i <= 3; i++ {
|
2026-01-29 16:44:01 +00:00
|
|
|
versionPURL := "pkg:npm/test@1.0." + string(rune('0'+i))
|
2026-01-29 16:06:56 +00:00
|
|
|
v := &Version{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: versionPURL,
|
|
|
|
|
PackagePURL: "pkg:npm/test",
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertVersion(v)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
a := &Artifact{
|
2026-01-29 16:44:01 +00:00
|
|
|
VersionPURL: versionPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Filename: "test.tgz",
|
|
|
|
|
UpstreamURL: "https://example.com/test.tgz",
|
|
|
|
|
StoragePath: sql.NullString{String: "/cache/test" + string(rune('0'+i)) + ".tgz", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: int64(i * 1000), Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertArtifact(a)
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
total, err := db.GetTotalCacheSize()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetTotalCacheSize failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if total != 6000 {
|
|
|
|
|
t.Errorf("expected total size 6000, got %d", total)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
count, err := db.GetCachedArtifactCount()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetCachedArtifactCount failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if count != 3 {
|
|
|
|
|
t.Errorf("expected 3 cached artifacts, got %d", count)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
lru, err := db.GetLeastRecentlyUsedArtifacts(2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetLeastRecentlyUsedArtifacts failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(lru) != 2 {
|
|
|
|
|
t.Errorf("expected 2 LRU artifacts, got %d", len(lru))
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestExists(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
if Exists(dbPath) {
|
|
|
|
|
t.Error("expected file to not exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, _ := os.Create(dbPath)
|
|
|
|
|
_ = f.Close()
|
|
|
|
|
|
|
|
|
|
if !Exists(dbPath) {
|
|
|
|
|
t.Error("expected file to exist")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGetCacheStats(t *testing.T) {
|
2026-01-29 16:06:56 +00:00
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
stats, err := db.GetCacheStats()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetCacheStats failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if stats.TotalPackages != 0 {
|
|
|
|
|
t.Errorf("expected 0 packages, got %d", stats.TotalPackages)
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
for _, eco := range []string{"npm", "cargo"} {
|
|
|
|
|
for i := 1; i <= 2; i++ {
|
|
|
|
|
name := eco + "-pkg" + string(rune('0'+i))
|
2026-01-29 16:44:01 +00:00
|
|
|
pkgPURL := "pkg:" + eco + "/" + name
|
2026-01-29 16:06:56 +00:00
|
|
|
pkg := &Package{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: pkgPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Ecosystem: eco,
|
|
|
|
|
Name: name,
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://example.com/" + name, Valid: true},
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertPackage(pkg)
|
2026-01-29 16:06:56 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
versionPURL := pkgPURL + "@1.0.0"
|
2026-01-29 16:06:56 +00:00
|
|
|
v := &Version{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: versionPURL,
|
|
|
|
|
PackagePURL: pkgPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertVersion(v)
|
2026-01-29 16:06:56 +00:00
|
|
|
|
|
|
|
|
a := &Artifact{
|
2026-01-29 16:44:01 +00:00
|
|
|
VersionPURL: versionPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Filename: name + ".tgz",
|
|
|
|
|
UpstreamURL: "https://example.com/" + name + ".tgz",
|
|
|
|
|
StoragePath: sql.NullString{String: "/cache/" + name + ".tgz", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 1000, Valid: true},
|
|
|
|
|
HitCount: int64(i),
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertArtifact(a)
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err = db.GetCacheStats()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetCacheStats failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if stats.TotalPackages != 4 {
|
|
|
|
|
t.Errorf("expected 4 packages, got %d", stats.TotalPackages)
|
|
|
|
|
}
|
|
|
|
|
if stats.TotalVersions != 4 {
|
|
|
|
|
t.Errorf("expected 4 versions, got %d", stats.TotalVersions)
|
|
|
|
|
}
|
|
|
|
|
if stats.TotalArtifacts != 4 {
|
|
|
|
|
t.Errorf("expected 4 artifacts, got %d", stats.TotalArtifacts)
|
|
|
|
|
}
|
|
|
|
|
if stats.TotalSize != 4000 {
|
|
|
|
|
t.Errorf("expected size 4000, got %d", stats.TotalSize)
|
|
|
|
|
}
|
|
|
|
|
if stats.TotalHits != 6 {
|
|
|
|
|
t.Errorf("expected 6 hits, got %d", stats.TotalHits)
|
|
|
|
|
}
|
|
|
|
|
if stats.EcosystemCounts["npm"] != 2 {
|
|
|
|
|
t.Errorf("expected 2 npm packages, got %d", stats.EcosystemCounts["npm"])
|
|
|
|
|
}
|
|
|
|
|
if stats.EcosystemCounts["cargo"] != 2 {
|
|
|
|
|
t.Errorf("expected 2 cargo packages, got %d", stats.EcosystemCounts["cargo"])
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
func TestGetMostPopularPackages(t *testing.T) {
|
|
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
for i := 1; i <= 3; i++ {
|
2026-01-29 16:44:01 +00:00
|
|
|
pkgPURL := "pkg:npm/pkg" + string(rune('0'+i))
|
2026-01-20 21:52:44 +00:00
|
|
|
pkg := &Package{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: pkgPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "pkg" + string(rune('0'+i)),
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://example.com", Valid: true},
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertPackage(pkg)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
versionPURL := pkgPURL + "@1.0.0"
|
2026-01-20 21:52:44 +00:00
|
|
|
v := &Version{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: versionPURL,
|
|
|
|
|
PackagePURL: pkgPURL,
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertVersion(v)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
|
|
|
|
a := &Artifact{
|
2026-01-29 16:44:01 +00:00
|
|
|
VersionPURL: versionPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Filename: "test.tgz",
|
|
|
|
|
UpstreamURL: "https://example.com/test.tgz",
|
|
|
|
|
StoragePath: sql.NullString{String: "/cache/test" + string(rune('0'+i)), Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: int64(i * 100), Valid: true},
|
|
|
|
|
HitCount: int64(i * 10),
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertArtifact(a)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
popular, err := db.GetMostPopularPackages(2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMostPopularPackages failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(popular) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 packages, got %d", len(popular))
|
|
|
|
|
}
|
|
|
|
|
if popular[0].Hits != 30 {
|
|
|
|
|
t.Errorf("expected first package to have 30 hits, got %d", popular[0].Hits)
|
|
|
|
|
}
|
|
|
|
|
if popular[1].Hits != 20 {
|
|
|
|
|
t.Errorf("expected second package to have 20 hits, got %d", popular[1].Hits)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
func TestGetRecentlyCachedPackages(t *testing.T) {
|
|
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
for i := 1; i <= 3; i++ {
|
2026-01-29 16:44:01 +00:00
|
|
|
pkgPURL := "pkg:npm/recent" + string(rune('0'+i))
|
2026-01-29 16:06:56 +00:00
|
|
|
pkg := &Package{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: pkgPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "recent" + string(rune('0'+i)),
|
2026-01-29 16:44:01 +00:00
|
|
|
RegistryURL: sql.NullString{String: "https://example.com", Valid: true},
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertPackage(pkg)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:44:01 +00:00
|
|
|
versionPURL := pkgPURL + "@1.0.0"
|
2026-01-29 16:06:56 +00:00
|
|
|
v := &Version{
|
2026-01-29 16:44:01 +00:00
|
|
|
PURL: versionPURL,
|
|
|
|
|
PackagePURL: pkgPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertVersion(v)
|
2026-01-29 16:06:56 +00:00
|
|
|
|
|
|
|
|
a := &Artifact{
|
2026-01-29 16:44:01 +00:00
|
|
|
VersionPURL: versionPURL,
|
2026-01-29 16:06:56 +00:00
|
|
|
Filename: "test.tgz",
|
|
|
|
|
UpstreamURL: "https://example.com/test.tgz",
|
|
|
|
|
StoragePath: sql.NullString{String: "/cache/recent" + string(rune('0'+i)), Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 1000, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: now.Add(time.Duration(-i) * time.Hour), Valid: true},
|
|
|
|
|
}
|
2026-01-29 16:44:01 +00:00
|
|
|
_ = db.UpsertArtifact(a)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
recent, err := db.GetRecentlyCachedPackages(2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetRecentlyCachedPackages failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(recent) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 packages, got %d", len(recent))
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
if recent[0].Name != "recent1" {
|
|
|
|
|
t.Errorf("expected first recent package to be recent1, got %s", recent[0].Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
func TestPostgresConnection(t *testing.T) {
|
|
|
|
|
url := os.Getenv("PROXY_DATABASE_URL")
|
|
|
|
|
if url == "" {
|
|
|
|
|
t.Skip("PROXY_DATABASE_URL not set, skipping postgres connection test")
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
db, err := OpenPostgresOrCreate(url)
|
2026-01-20 21:52:44 +00:00
|
|
|
if err != nil {
|
2026-01-29 16:06:56 +00:00
|
|
|
t.Fatalf("OpenPostgresOrCreate failed: %v", err)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
if db.Dialect() != DialectPostgres {
|
|
|
|
|
t.Errorf("expected postgres dialect, got %s", db.Dialect())
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
|
|
|
|
|
version, err := db.SchemaVersion()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SchemaVersion failed: %v", err)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
if version != SchemaVersion {
|
|
|
|
|
t.Errorf("expected schema version %d, got %d", SchemaVersion, version)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
func createTestDB(t *testing.T) *DB {
|
|
|
|
|
t.Helper()
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return db
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
func createTestPostgresDB(t *testing.T) *DB {
|
|
|
|
|
t.Helper()
|
|
|
|
|
url := os.Getenv("PROXY_DATABASE_URL")
|
|
|
|
|
if url == "" {
|
|
|
|
|
return nil
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 16:06:56 +00:00
|
|
|
db, err := OpenPostgres(url)
|
2026-01-20 21:52:44 +00:00
|
|
|
if err != nil {
|
2026-01-29 16:06:56 +00:00
|
|
|
t.Fatalf("OpenPostgres failed: %v", err)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
|
|
|
|
|
// Drop and recreate tables for clean test state
|
|
|
|
|
tables := []string{"artifacts", "versions", "packages", "schema_info"}
|
|
|
|
|
for _, table := range tables {
|
|
|
|
|
_, _ = db.Exec("DROP TABLE IF EXISTS " + table + " CASCADE")
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
|
|
|
|
|
if err := db.CreateSchema(); err != nil {
|
|
|
|
|
_ = db.Close()
|
|
|
|
|
t.Fatalf("CreateSchema failed: %v", err)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-01-29 16:06:56 +00:00
|
|
|
|
|
|
|
|
return db
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runWithBothDatabases(t *testing.T, testFunc func(t *testing.T, db *DB)) {
|
|
|
|
|
t.Run("sqlite", func(t *testing.T) {
|
|
|
|
|
db := createTestDB(t)
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
testFunc(t, db)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("postgres", func(t *testing.T) {
|
|
|
|
|
db := createTestPostgresDB(t)
|
|
|
|
|
if db == nil {
|
|
|
|
|
t.Skip("PROXY_DATABASE_URL not set, skipping postgres test")
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
testFunc(t, db)
|
|
|
|
|
})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-02-03 22:40:23 +00:00
|
|
|
|
|
|
|
|
// TestMigrationFromOldSchema tests that we can migrate from an old schema
|
|
|
|
|
// that's missing columns like enriched_at, registry_url, etc.
|
|
|
|
|
func TestMigrationFromOldSchema(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "old.db")
|
|
|
|
|
|
|
|
|
|
// Create a database with old schema (missing new columns)
|
|
|
|
|
sqlDB, err := sql.Open("sqlite", dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to open database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldSchema := `
|
|
|
|
|
CREATE TABLE packages (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
purl TEXT NOT NULL,
|
|
|
|
|
ecosystem TEXT NOT NULL,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
latest_version TEXT,
|
|
|
|
|
license TEXT,
|
|
|
|
|
description TEXT,
|
|
|
|
|
homepage TEXT,
|
|
|
|
|
repository_url TEXT,
|
|
|
|
|
created_at DATETIME,
|
|
|
|
|
updated_at DATETIME
|
|
|
|
|
);
|
|
|
|
|
CREATE UNIQUE INDEX idx_packages_purl ON packages(purl);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE versions (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
purl TEXT NOT NULL,
|
|
|
|
|
package_purl TEXT NOT NULL,
|
|
|
|
|
license TEXT,
|
|
|
|
|
published_at DATETIME,
|
|
|
|
|
created_at DATETIME,
|
|
|
|
|
updated_at DATETIME
|
|
|
|
|
);
|
|
|
|
|
CREATE UNIQUE INDEX idx_versions_purl ON versions(purl);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE artifacts (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
version_purl TEXT NOT NULL,
|
|
|
|
|
filename TEXT NOT NULL,
|
|
|
|
|
upstream_url TEXT NOT NULL,
|
|
|
|
|
storage_path TEXT,
|
|
|
|
|
content_hash TEXT,
|
|
|
|
|
size INTEGER,
|
|
|
|
|
content_type TEXT,
|
|
|
|
|
fetched_at DATETIME,
|
|
|
|
|
hit_count INTEGER DEFAULT 0,
|
|
|
|
|
last_accessed_at DATETIME,
|
|
|
|
|
created_at DATETIME,
|
|
|
|
|
updated_at DATETIME
|
|
|
|
|
);
|
|
|
|
|
CREATE UNIQUE INDEX idx_artifacts_version_filename ON artifacts(version_purl, filename);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE schema_info (version INTEGER NOT NULL);
|
|
|
|
|
INSERT INTO schema_info (version) VALUES (1);
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
if _, err := sqlDB.Exec(oldSchema); err != nil {
|
|
|
|
|
t.Fatalf("failed to create old schema: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert test data
|
|
|
|
|
now := time.Now()
|
|
|
|
|
_, err = sqlDB.Exec(`
|
|
|
|
|
INSERT INTO packages (purl, ecosystem, name, latest_version, license, created_at, updated_at)
|
|
|
|
|
VALUES ('pkg:npm/test-package', 'npm', 'test-package', '1.0.0', 'MIT', ?, ?)
|
|
|
|
|
`, now, now)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to insert test package: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = sqlDB.Exec(`
|
|
|
|
|
INSERT INTO versions (purl, package_purl, license, created_at, updated_at)
|
|
|
|
|
VALUES ('pkg:npm/test-package@1.0.0', 'pkg:npm/test-package', 'MIT', ?, ?)
|
|
|
|
|
`, now, now)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to insert test version: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = sqlDB.Exec(`
|
|
|
|
|
INSERT INTO artifacts (version_purl, filename, upstream_url, storage_path, size, fetched_at, hit_count, created_at, updated_at)
|
|
|
|
|
VALUES ('pkg:npm/test-package@1.0.0', 'test-package-1.0.0.tgz', 'https://registry.npmjs.org/test-package/-/test-package-1.0.0.tgz', '/path/to/artifact', 1024, ?, 5, ?, ?)
|
|
|
|
|
`, now, now, now)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to insert test artifact: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := sqlDB.Close(); err != nil {
|
|
|
|
|
t.Fatalf("failed to close database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open with our DB wrapper
|
|
|
|
|
db, err := Open(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to open database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
2026-04-06 13:06:45 +01:00
|
|
|
// Queries that require new columns should fail without migration
|
|
|
|
|
if _, err := db.GetEnrichmentStats(); err == nil {
|
|
|
|
|
t.Error("GetEnrichmentStats: expected error querying enriched_at column, got nil")
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.GetPackageByEcosystemName("npm", "test-package"); err == nil {
|
|
|
|
|
t.Error("GetPackageByEcosystemName: expected error querying registry_url column, got nil")
|
|
|
|
|
}
|
|
|
|
|
// SearchPackages should work even with old schema because it uses sql.NullString
|
|
|
|
|
if _, err := db.SearchPackages("test", "", 10, 0); err != nil {
|
|
|
|
|
t.Errorf("SearchPackages: unexpected error with old schema: %v", err)
|
|
|
|
|
}
|
2026-02-03 22:40:23 +00:00
|
|
|
|
|
|
|
|
// Run migration
|
2026-04-06 13:06:45 +01:00
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
2026-02-03 22:40:23 +00:00
|
|
|
|
|
|
|
|
// Verify queries work after migration
|
2026-04-06 13:06:45 +01:00
|
|
|
stats, err := db.GetEnrichmentStats()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("GetEnrichmentStats failed after migration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if stats == nil {
|
|
|
|
|
t.Error("GetEnrichmentStats returned nil after migration")
|
|
|
|
|
}
|
2026-02-03 22:40:23 +00:00
|
|
|
|
2026-04-06 13:06:45 +01:00
|
|
|
pkg, err := db.GetPackageByEcosystemName("npm", "test-package")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("GetPackageByEcosystemName failed after migration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if pkg == nil {
|
|
|
|
|
t.Fatal("GetPackageByEcosystemName returned nil after migration")
|
|
|
|
|
}
|
|
|
|
|
if pkg.Name != "test-package" {
|
|
|
|
|
t.Errorf("expected package name test-package, got %s", pkg.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify migrations were recorded
|
|
|
|
|
applied, err := db.appliedMigrations()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("appliedMigrations failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for _, m := range migrations {
|
|
|
|
|
if !applied[m.name] {
|
|
|
|
|
t.Errorf("migration %s not recorded as applied", m.name)
|
2026-02-03 22:40:23 +00:00
|
|
|
}
|
2026-04-06 13:06:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Running again should be a no-op
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("second MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFreshDatabaseRecordsMigrations(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "fresh.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
applied, err := db.appliedMigrations()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("appliedMigrations failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, m := range migrations {
|
|
|
|
|
if !applied[m.name] {
|
|
|
|
|
t.Errorf("migration %s not recorded in fresh database", m.name)
|
2026-02-03 22:40:23 +00:00
|
|
|
}
|
2026-04-06 13:06:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMigrateSchemaSkipsApplied(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
// All migrations are already recorded from Create. Running MigrateSchema
|
|
|
|
|
// should return without running any migration functions.
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify count hasn't changed (no duplicate inserts)
|
|
|
|
|
var count int
|
|
|
|
|
if err := db.Get(&count, "SELECT COUNT(*) FROM migrations"); err != nil {
|
|
|
|
|
t.Fatalf("counting migrations failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if count != len(migrations) {
|
|
|
|
|
t.Errorf("expected %d migrations, got %d", len(migrations), count)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMigrateSchemaUpgradeFromFullyMigrated(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "existing.db")
|
|
|
|
|
|
|
|
|
|
// Simulate an existing proxy database that has the full current schema
|
|
|
|
|
// but no migrations table (i.e. it was running the previous version).
|
|
|
|
|
sqlDB, err := sql.Open("sqlite", dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to open database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := sqlDB.Exec(schemaSQLite); err != nil {
|
|
|
|
|
t.Fatalf("failed to create schema: %v", err)
|
|
|
|
|
}
|
|
|
|
|
// Drop the migrations table that schemaSQLite now includes
|
|
|
|
|
if _, err := sqlDB.Exec("DROP TABLE migrations"); err != nil {
|
|
|
|
|
t.Fatalf("failed to drop migrations table: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := sqlDB.Exec("INSERT INTO schema_info (version) VALUES (1)"); err != nil {
|
|
|
|
|
t.Fatalf("failed to set schema version: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := sqlDB.Close(); err != nil {
|
|
|
|
|
t.Fatalf("failed to close database: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db, err := Open(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Open failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
// This should create the migrations table and record all migrations
|
|
|
|
|
// without altering any tables (everything already exists).
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applied, err := db.appliedMigrations()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("appliedMigrations failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for _, m := range migrations {
|
|
|
|
|
if !applied[m.name] {
|
|
|
|
|
t.Errorf("migration %s not recorded after upgrade", m.name)
|
2026-02-03 22:40:23 +00:00
|
|
|
}
|
2026-04-06 13:06:45 +01:00
|
|
|
}
|
2026-02-03 22:40:23 +00:00
|
|
|
|
2026-04-06 13:06:45 +01:00
|
|
|
// Second run should be the fast path (single SELECT)
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("second MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
2026-02-03 22:40:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConcurrentWrites(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/test",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "test",
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertPackage(pkg); err != nil {
|
|
|
|
|
t.Fatalf("UpsertPackage failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ver := &Version{
|
|
|
|
|
PURL: "pkg:npm/test@1.0.0",
|
|
|
|
|
PackagePURL: pkg.PURL,
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertVersion(ver); err != nil {
|
|
|
|
|
t.Fatalf("UpsertVersion failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
done := make(chan error, 10)
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
go func() {
|
|
|
|
|
artifact := &Artifact{
|
|
|
|
|
VersionPURL: ver.PURL,
|
|
|
|
|
Filename: "test.tgz",
|
|
|
|
|
UpstreamURL: "https://example.com/test.tgz",
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertArtifact(artifact); err != nil {
|
|
|
|
|
done <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
done <- nil
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
if err := <-done; err != nil {
|
|
|
|
|
t.Errorf("concurrent write %d failed: %v", i, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSearchPackagesWithNulls(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/test-package",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "test-package",
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertPackage(pkg); err != nil {
|
|
|
|
|
t.Fatalf("UpsertPackage failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ver := &Version{
|
|
|
|
|
PURL: "pkg:npm/test-package@1.0.0",
|
|
|
|
|
PackagePURL: pkg.PURL,
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertVersion(ver); err != nil {
|
|
|
|
|
t.Fatalf("UpsertVersion failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifact := &Artifact{
|
|
|
|
|
VersionPURL: ver.PURL,
|
|
|
|
|
Filename: "test-package-1.0.0.tgz",
|
|
|
|
|
UpstreamURL: "https://registry.npmjs.org/test-package/-/test-package-1.0.0.tgz",
|
|
|
|
|
StoragePath: sql.NullString{String: "./cache/test.tgz", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 1024, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
HitCount: 5,
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertArtifact(artifact); err != nil {
|
|
|
|
|
t.Fatalf("UpsertArtifact failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results, err := db.SearchPackages("test", "", 10, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SearchPackages failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(results) != 1 {
|
|
|
|
|
t.Fatalf("expected 1 result, got %d", len(results))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := results[0]
|
|
|
|
|
if result.Ecosystem != "npm" {
|
|
|
|
|
t.Errorf("expected ecosystem npm, got %s", result.Ecosystem)
|
|
|
|
|
}
|
|
|
|
|
if result.Name != "test-package" {
|
|
|
|
|
t.Errorf("expected name test-package, got %s", result.Name)
|
|
|
|
|
}
|
|
|
|
|
if result.LatestVersion.Valid {
|
|
|
|
|
t.Errorf("expected LatestVersion to be null, got %s", result.LatestVersion.String)
|
|
|
|
|
}
|
|
|
|
|
if result.License.Valid {
|
|
|
|
|
t.Errorf("expected License to be null, got %s", result.License.String)
|
|
|
|
|
}
|
|
|
|
|
if result.Hits != 5 {
|
|
|
|
|
t.Errorf("expected 5 hits, got %d", result.Hits)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSearchPackagesWithValues(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
PURL: "pkg:npm/licensed-package",
|
|
|
|
|
Ecosystem: "npm",
|
|
|
|
|
Name: "licensed-package",
|
|
|
|
|
LatestVersion: sql.NullString{String: "2.0.0", Valid: true},
|
|
|
|
|
License: sql.NullString{String: "MIT", Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertPackage(pkg); err != nil {
|
|
|
|
|
t.Fatalf("UpsertPackage failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ver := &Version{
|
|
|
|
|
PURL: "pkg:npm/licensed-package@1.0.0",
|
|
|
|
|
PackagePURL: pkg.PURL,
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertVersion(ver); err != nil {
|
|
|
|
|
t.Fatalf("UpsertVersion failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifact := &Artifact{
|
|
|
|
|
VersionPURL: ver.PURL,
|
|
|
|
|
Filename: "licensed-package-1.0.0.tgz",
|
|
|
|
|
UpstreamURL: "https://registry.npmjs.org/licensed-package/-/licensed-package-1.0.0.tgz",
|
|
|
|
|
StoragePath: sql.NullString{String: "./cache/licensed.tgz", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 2048, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
HitCount: 10,
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertArtifact(artifact); err != nil {
|
|
|
|
|
t.Fatalf("UpsertArtifact failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results, err := db.SearchPackages("licensed", "", 10, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SearchPackages failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(results) != 1 {
|
|
|
|
|
t.Fatalf("expected 1 result, got %d", len(results))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := results[0]
|
|
|
|
|
if result.Ecosystem != "npm" {
|
|
|
|
|
t.Errorf("expected ecosystem npm, got %s", result.Ecosystem)
|
|
|
|
|
}
|
|
|
|
|
if result.Name != "licensed-package" {
|
|
|
|
|
t.Errorf("expected name licensed-package, got %s", result.Name)
|
|
|
|
|
}
|
|
|
|
|
if !result.LatestVersion.Valid || result.LatestVersion.String != "2.0.0" {
|
|
|
|
|
t.Errorf("expected LatestVersion 2.0.0, got valid=%v value=%s", result.LatestVersion.Valid, result.LatestVersion.String)
|
|
|
|
|
}
|
|
|
|
|
if !result.License.Valid || result.License.String != "MIT" {
|
|
|
|
|
t.Errorf("expected License MIT, got valid=%v value=%s", result.License.Valid, result.License.String)
|
|
|
|
|
}
|
|
|
|
|
if result.Hits != 10 {
|
|
|
|
|
t.Errorf("expected 10 hits, got %d", result.Hits)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-06 13:06:45 +01:00
|
|
|
|
|
|
|
|
func BenchmarkMigrateSchemaFullyMigrated(b *testing.B) {
|
|
|
|
|
dir := b.TempDir()
|
|
|
|
|
dbPath := filepath.Join(dir, "bench.db")
|
|
|
|
|
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
// First call to ensure everything is migrated
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
b.Fatalf("initial MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
for b.Loop() {
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
b.Fatalf("MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|