335 lines
10 KiB
Go
335 lines
10 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
var (
|
|
testDB *DB
|
|
testDSN string
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
c := context.Background()
|
|
dsn, terminate, err := testsupport.StartPostgres(c)
|
|
if err != nil {
|
|
// Docker unavailable: run anyway so tests self-skip via requireDB.
|
|
os.Exit(m.Run())
|
|
}
|
|
testDSN = dsn
|
|
db, err := New(dsn)
|
|
if err != nil {
|
|
terminate()
|
|
panic(err)
|
|
}
|
|
testDB = db
|
|
|
|
code := m.Run()
|
|
|
|
db.Close()
|
|
terminate()
|
|
// Return normally on success so the coverage profile is flushed; os.Exit
|
|
// would truncate it.
|
|
if code != 0 {
|
|
os.Exit(code)
|
|
}
|
|
}
|
|
|
|
func requireDB(t *testing.T) {
|
|
t.Helper()
|
|
if testDB == nil {
|
|
t.Skip("Docker unavailable; skipping database integration test")
|
|
}
|
|
}
|
|
|
|
func ctx() context.Context { return context.Background() }
|
|
|
|
func seedRemote(t *testing.T, name string) {
|
|
t.Helper()
|
|
if err := testDB.CreateRemote(ctx(), &models.Remote{
|
|
Name: name, PackageType: models.PackageGeneric, RepoType: models.RepoTypeRemote,
|
|
BaseURL: "https://example.com", MutableTTL: 3600,
|
|
}); err != nil {
|
|
t.Fatalf("seed remote: %v", err)
|
|
}
|
|
}
|
|
|
|
// seedBlob inserts a blob and returns its full content hash (sha256:<hash>),
|
|
// matching the reference convention used by artifacts and local files.
|
|
func seedBlob(t *testing.T, hash string) string {
|
|
t.Helper()
|
|
full := "sha256:" + hash
|
|
if err := testDB.UpsertBlob(ctx(), full, "blobs/sha256/"+hash, 10, "application/octet-stream"); err != nil {
|
|
t.Fatalf("seed blob: %v", err)
|
|
}
|
|
return full
|
|
}
|
|
|
|
func TestRemotesCRUD(t *testing.T) {
|
|
requireDB(t)
|
|
seedRemote(t, "r-crud")
|
|
got, err := testDB.GetRemote(ctx(), "r-crud")
|
|
if err != nil || got.BaseURL != "https://example.com" {
|
|
t.Fatalf("get: %v %v", got, err)
|
|
}
|
|
got.BaseURL = "https://updated.example.com"
|
|
if err := testDB.UpdateRemote(ctx(), got); err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
got, _ = testDB.GetRemote(ctx(), "r-crud")
|
|
if got.BaseURL != "https://updated.example.com" {
|
|
t.Errorf("update not applied: %v", got.BaseURL)
|
|
}
|
|
list, err := testDB.ListRemotes(ctx())
|
|
if err != nil || len(list) == 0 {
|
|
t.Fatalf("list: %v %v", len(list), err)
|
|
}
|
|
if err := testDB.DeleteRemote(ctx(), "r-crud"); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if _, err := testDB.GetRemote(ctx(), "r-crud"); err == nil {
|
|
t.Error("expected error after delete")
|
|
}
|
|
}
|
|
|
|
func TestArtifactsAndBlobs(t *testing.T) {
|
|
requireDB(t)
|
|
seedRemote(t, "r-art")
|
|
seedBlob(t, "aaaa")
|
|
hash := "sha256:aaaa"
|
|
if err := testDB.UpsertBlob(ctx(), hash, "blobs/sha256/aaaa", 10, "text/plain"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := testDB.UpsertArtifact(ctx(), "r-art", "path/a.txt", hash, "etag1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Upsert again to exercise the ON CONFLICT update branch.
|
|
if err := testDB.UpsertArtifact(ctx(), "r-art", "path/a.txt", hash, "etag2"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
art, err := testDB.GetArtifact(ctx(), "r-art", "path/a.txt")
|
|
if err != nil || art.ContentHash != hash {
|
|
t.Fatalf("get artifact: %v %v", art, err)
|
|
}
|
|
if err := testDB.TouchArtifactAccess(ctx(), "r-art", "path/a.txt"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
arts, err := testDB.ListArtifacts(ctx(), "r-art", 10, 0)
|
|
if err != nil || len(arts) != 1 {
|
|
t.Fatalf("list artifacts: %v %v", len(arts), err)
|
|
}
|
|
if err := testDB.InsertAccessLog(ctx(), "r-art", "path/a.txt", true, 10, 5, "1.2.3.4"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := testDB.InsertAccessLogBatch(ctx(), []AccessLogEntry{
|
|
{RemoteName: "r-art", Path: "b", CacheHit: false, SizeBytes: 20, UpstreamMS: 3},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := testDB.InsertAccessLogBatch(ctx(), nil); err != nil {
|
|
t.Fatalf("empty batch should be a no-op: %v", err)
|
|
}
|
|
if err := testDB.DeleteArtifact(ctx(), "r-art", "path/a.txt"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestOrphanAndColdCleanup(t *testing.T) {
|
|
requireDB(t)
|
|
seedBlob(t, "orphanhash")
|
|
// A blob with no artifact/local_file reference is orphaned, but only past
|
|
// the grace period.
|
|
if got, _ := testDB.FindOrphanedBlobs(ctx(), time.Hour); containsHash(got, "sha256:orphanhash") {
|
|
t.Error("fresh orphan should be excluded by grace period")
|
|
}
|
|
orphans, err := testDB.FindOrphanedBlobs(ctx(), -time.Hour) // cutoff in the future => include fresh
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !containsHash(orphans, "sha256:orphanhash") {
|
|
t.Error("expected orphan to be found with zero grace")
|
|
}
|
|
if err := testDB.DeleteBlob(ctx(), "sha256:orphanhash"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
seedRemote(t, "r-cold")
|
|
seedBlob(t, "coldhash")
|
|
testDB.UpsertArtifact(ctx(), "r-cold", "cold.txt", "sha256:coldhash", "")
|
|
n, err := testDB.DeleteColdArtifacts(ctx(), "r-cold", -time.Hour) // negative => everything is "cold"
|
|
if err != nil || n < 1 {
|
|
t.Fatalf("delete cold: n=%d err=%v", n, err)
|
|
}
|
|
}
|
|
|
|
func containsHash(blobs []models.Blob, hash string) bool {
|
|
for _, b := range blobs {
|
|
if b.ContentHash == hash {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestLocalFiles(t *testing.T) {
|
|
requireDB(t)
|
|
seedRemote(t, "r-local")
|
|
seedBlob(t, "localhash")
|
|
hash := "sha256:localhash"
|
|
if err := testDB.CreateLocalFile(ctx(), "r-local", "foo/foo-1.0.whl", hash); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Duplicate create must be rejected.
|
|
if err := testDB.CreateLocalFile(ctx(), "r-local", "foo/foo-1.0.whl", hash); err == nil {
|
|
t.Error("expected duplicate local file error")
|
|
}
|
|
f, err := testDB.GetLocalFile(ctx(), "r-local", "foo/foo-1.0.whl")
|
|
if err != nil || f == nil {
|
|
t.Fatalf("get local file: %v %v", f, err)
|
|
}
|
|
if files, err := testDB.ListLocalFiles(ctx(), "r-local", 10, 0); err != nil || len(files) != 1 {
|
|
t.Fatalf("list: %v %v", len(files), err)
|
|
}
|
|
if files, err := testDB.ListLocalFilesByPrefix(ctx(), "r-local", "foo/"); err != nil || len(files) != 1 {
|
|
t.Fatalf("list by prefix: %v %v", len(files), err)
|
|
}
|
|
if entries, err := testDB.ListFilesByPrefix(ctx(), "r-local", "foo/"); err != nil || len(entries) != 1 {
|
|
t.Fatalf("provider list by prefix: %v %v", len(entries), err)
|
|
}
|
|
if pkgs, err := testDB.ListLocalFilePackages(ctx(), "r-local"); err != nil || len(pkgs) == 0 {
|
|
t.Fatalf("list packages: %v %v", pkgs, err)
|
|
}
|
|
if pkgs, err := testDB.ListPackages(ctx(), "r-local"); err != nil || len(pkgs) == 0 {
|
|
t.Fatalf("provider list packages: %v %v", pkgs, err)
|
|
}
|
|
if err := testDB.DeleteLocalFile(ctx(), "r-local", "foo/foo-1.0.whl"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestVirtualsCRUD(t *testing.T) {
|
|
requireDB(t)
|
|
if err := testDB.CreateVirtual(ctx(), &models.Virtual{
|
|
Name: "v-crud", PackageType: models.PackageHelm, Members: []string{"a", "b"},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v, err := testDB.GetVirtual(ctx(), "v-crud")
|
|
if err != nil || len(v.Members) != 2 {
|
|
t.Fatalf("get virtual: %v %v", v, err)
|
|
}
|
|
v.Members = []string{"a"}
|
|
if err := testDB.UpdateVirtual(ctx(), v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if vs, err := testDB.ListVirtuals(ctx()); err != nil || len(vs) == 0 {
|
|
t.Fatalf("list virtuals: %v %v", len(vs), err)
|
|
}
|
|
if err := testDB.DeleteVirtual(ctx(), "v-crud"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestStats(t *testing.T) {
|
|
requireDB(t)
|
|
seedRemote(t, "r-stats")
|
|
seedBlob(t, "statshash")
|
|
testDB.UpsertArtifact(ctx(), "r-stats", "s.txt", "sha256:statshash", "")
|
|
testDB.InsertAccessLog(ctx(), "r-stats", "s.txt", true, 100, 2, "")
|
|
|
|
if _, err := testDB.GetOverviewStats(ctx()); err != nil {
|
|
t.Fatalf("overview: %v", err)
|
|
}
|
|
if _, err := testDB.GetTopRemotes(ctx(), 5); err != nil {
|
|
t.Fatalf("top remotes: %v", err)
|
|
}
|
|
if _, err := testDB.GetTopFilesByHits(ctx(), 5); err != nil {
|
|
t.Fatalf("top files by hits: %v", err)
|
|
}
|
|
if _, err := testDB.GetTopFilesByBandwidth(ctx(), 5); err != nil {
|
|
t.Fatalf("top files by bandwidth: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDatabaseErrorPaths(t *testing.T) {
|
|
requireDB(t)
|
|
bad, err := New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bad.Close() // every query now fails
|
|
ctx := context.Background()
|
|
|
|
if _, err := bad.ListRemotes(ctx); err == nil {
|
|
t.Error("ListRemotes should error on closed db")
|
|
}
|
|
if _, err := bad.ListVirtuals(ctx); err == nil {
|
|
t.Error("ListVirtuals should error")
|
|
}
|
|
if _, err := bad.ListArtifacts(ctx, "r", 10, 0); err == nil {
|
|
t.Error("ListArtifacts should error")
|
|
}
|
|
if _, err := bad.ListLocalFiles(ctx, "r", 10, 0); err == nil {
|
|
t.Error("ListLocalFiles should error")
|
|
}
|
|
if _, err := bad.ListLocalFilesByPrefix(ctx, "r", "p"); err == nil {
|
|
t.Error("ListLocalFilesByPrefix should error")
|
|
}
|
|
if _, err := bad.ListLocalFilePackages(ctx, "r"); err == nil {
|
|
t.Error("ListLocalFilePackages should error")
|
|
}
|
|
if _, err := bad.ListFilesByPrefix(ctx, "r", "p"); err == nil {
|
|
t.Error("ListFilesByPrefix should error")
|
|
}
|
|
if _, err := bad.ListPackages(ctx, "r"); err == nil {
|
|
t.Error("ListPackages should error")
|
|
}
|
|
if _, err := bad.FindOrphanedBlobs(ctx, 0); err == nil {
|
|
t.Error("FindOrphanedBlobs should error")
|
|
}
|
|
if _, err := bad.GetOverviewStats(ctx); err == nil {
|
|
t.Error("GetOverviewStats should error")
|
|
}
|
|
if _, err := bad.GetTopRemotes(ctx, 5); err == nil {
|
|
t.Error("GetTopRemotes should error")
|
|
}
|
|
if _, err := bad.GetTopFilesByHits(ctx, 5); err == nil {
|
|
t.Error("GetTopFilesByHits should error")
|
|
}
|
|
if _, err := bad.GetTopFilesByBandwidth(ctx, 5); err == nil {
|
|
t.Error("GetTopFilesByBandwidth should error")
|
|
}
|
|
if _, err := bad.ListRPMMetadataEntries(ctx, "r"); err == nil {
|
|
t.Error("ListRPMMetadataEntries should error")
|
|
}
|
|
}
|
|
|
|
func TestRPMMetadata(t *testing.T) {
|
|
requireDB(t)
|
|
seedRemote(t, "r-rpm")
|
|
meta := &provider.RPMMetadata{
|
|
RepoName: "r-rpm", FilePath: "Packages/x.rpm", ContentHash: "sha256:rpm",
|
|
Name: "x", Version: "1.0", Release: "1", Arch: "noarch",
|
|
Requires: []provider.RPMDep{{Name: "libc"}},
|
|
Provides: []provider.RPMDep{{Name: "x"}},
|
|
Files: []provider.RPMFile{},
|
|
}
|
|
if err := testDB.InsertRPMMetadata(ctx(), meta); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, err := testDB.ListRPMMetadataEntries(ctx(), "r-rpm")
|
|
if err != nil || len(entries) != 1 {
|
|
t.Fatalf("list rpm entries: %v %v", len(entries), err)
|
|
}
|
|
if rows, err := testDB.ListRPMMetadata(ctx(), "r-rpm"); err != nil || len(rows) != 1 {
|
|
t.Fatalf("list rpm rows: %v %v", len(rows), err)
|
|
}
|
|
}
|