Files
at-container-registry/pkg/appview/db/batch_test.go
2026-04-19 18:04:57 -05:00

384 lines
12 KiB
Go

package db
import (
"database/sql"
"fmt"
"strings"
"testing"
"time"
)
// setupBatchTestDB spins up a fresh in-memory libsql database with the full
// schema applied, so every batch test can write realistic data without
// stubbing individual tables.
func setupBatchTestDB(t *testing.T) *sql.DB {
t.Helper()
safeName := strings.ReplaceAll(t.Name(), "/", "_")
d, err := InitDB(fmt.Sprintf("file:%s?mode=memory&cache=shared", safeName), LibsqlConfig{})
if err != nil {
t.Fatalf("init db: %v", err)
}
// Single conn to avoid cross-test contention in the shared in-memory cache.
d.SetMaxOpenConns(1)
t.Cleanup(func() { d.Close() })
return d
}
func createBatchTestUser(t *testing.T, d *sql.DB, did string) {
t.Helper()
_, err := d.Exec(`
INSERT OR IGNORE INTO users (did, handle, pds_endpoint, last_seen)
VALUES (?, ?, ?, datetime('now'))
`, did, did+".bsky.social", "https://pds.example.com")
if err != nil {
t.Fatalf("seed user: %v", err)
}
}
func countRows(t *testing.T, d *sql.DB, query string, args ...any) int {
t.Helper()
var n int
if err := d.QueryRow(query, args...).Scan(&n); err != nil {
t.Fatalf("count: %v", err)
}
return n
}
func TestBuildPlaceholders(t *testing.T) {
cases := []struct {
rows, cols int
want string
}{
{1, 1, "(?)"},
{2, 1, "(?),(?)"},
{1, 3, "(?,?,?)"},
{3, 2, "(?,?),(?,?),(?,?)"},
{0, 5, ""},
{5, 0, ""},
}
for _, c := range cases {
got := buildPlaceholders(c.rows, c.cols)
if got != c.want {
t.Errorf("buildPlaceholders(%d,%d) = %q, want %q", c.rows, c.cols, got, c.want)
}
}
}
func TestBatchInsertManifests_InsertsAndReturnsIDs(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
now := time.Now()
manifests := []Manifest{
{DID: "did:plc:alice", Repository: "app1", Digest: "sha256:aaa", HoldEndpoint: "did:web:hold", SchemaVersion: 2, MediaType: "application/vnd.oci.image.manifest.v1+json", ArtifactType: "container-image", CreatedAt: now},
{DID: "did:plc:alice", Repository: "app2", Digest: "sha256:bbb", HoldEndpoint: "did:web:hold", SchemaVersion: 2, MediaType: "application/vnd.oci.image.manifest.v1+json", ArtifactType: "container-image", CreatedAt: now},
}
ids, err := BatchInsertManifests(d, manifests)
if err != nil {
t.Fatalf("batch insert: %v", err)
}
if len(ids) != 2 {
t.Fatalf("expected 2 ids, got %d", len(ids))
}
if ids[ManifestKey("did:plc:alice", "app1", "sha256:aaa")] == 0 {
t.Errorf("missing id for app1")
}
if ids[ManifestKey("did:plc:alice", "app2", "sha256:bbb")] == 0 {
t.Errorf("missing id for app2")
}
if got := countRows(t, d, `SELECT COUNT(*) FROM manifests`); got != 2 {
t.Errorf("row count = %d, want 2", got)
}
}
func TestBatchInsertManifests_Idempotent(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
now := time.Now()
m := []Manifest{{
DID: "did:plc:alice", Repository: "app", Digest: "sha256:aaa",
HoldEndpoint: "did:web:hold", SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ArtifactType: "container-image", CreatedAt: now,
}}
if _, err := BatchInsertManifests(d, m); err != nil {
t.Fatalf("first insert: %v", err)
}
if _, err := BatchInsertManifests(d, m); err != nil {
t.Fatalf("second insert: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM manifests`); got != 1 {
t.Errorf("expected idempotent; row count = %d", got)
}
}
func TestBatchInsertManifests_Chunking(t *testing.T) {
// Exceed one sub-batch to exercise the chunk loop.
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
const n = BatchSize + 17
now := time.Now()
manifests := make([]Manifest, n)
for i := 0; i < n; i++ {
manifests[i] = Manifest{
DID: "did:plc:alice", Repository: "app", Digest: fmt.Sprintf("sha256:%04d", i),
HoldEndpoint: "did:web:hold", SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ArtifactType: "container-image", CreatedAt: now,
}
}
ids, err := BatchInsertManifests(d, manifests)
if err != nil {
t.Fatalf("batch insert: %v", err)
}
if len(ids) != n {
t.Errorf("ids len = %d, want %d", len(ids), n)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM manifests`); got != n {
t.Errorf("row count = %d, want %d", got, n)
}
}
func TestBatchInsertLayers_RespectsFK(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
now := time.Now()
ids, err := BatchInsertManifests(d, []Manifest{{
DID: "did:plc:alice", Repository: "app", Digest: "sha256:aaa",
HoldEndpoint: "did:web:hold", SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ArtifactType: "container-image", CreatedAt: now,
}})
if err != nil {
t.Fatalf("insert manifest: %v", err)
}
mid := ids[ManifestKey("did:plc:alice", "app", "sha256:aaa")]
layers := []Layer{
{ManifestID: mid, Digest: "sha256:L0", Size: 100, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", LayerIndex: 0},
{ManifestID: mid, Digest: "sha256:L1", Size: 200, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", LayerIndex: 1},
}
if err := BatchInsertLayers(d, layers); err != nil {
t.Fatalf("batch insert layers: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM layers`); got != 2 {
t.Errorf("layers count = %d, want 2", got)
}
// Re-run to confirm ON CONFLICT DO NOTHING doesn't error.
if err := BatchInsertLayers(d, layers); err != nil {
t.Fatalf("idempotent layers: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM layers`); got != 2 {
t.Errorf("layers after re-insert = %d, want 2", got)
}
}
func TestBatchUpsertTags_Idempotent(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
now := time.Now()
tags := []Tag{
{DID: "did:plc:alice", Repository: "app", Tag: "v1", Digest: "sha256:aaa", CreatedAt: now},
{DID: "did:plc:alice", Repository: "app", Tag: "v2", Digest: "sha256:bbb", CreatedAt: now},
}
if err := BatchUpsertTags(d, tags); err != nil {
t.Fatalf("batch upsert: %v", err)
}
if err := BatchUpsertTags(d, tags); err != nil {
t.Fatalf("rerun: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM tags`); got != 2 {
t.Errorf("tags count = %d, want 2", got)
}
}
func TestBatchUpsertStars(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
createBatchTestUser(t, d, "did:plc:bob")
now := time.Now()
stars := []StarInput{
{StarrerDID: "did:plc:bob", OwnerDID: "did:plc:alice", Repository: "app", CreatedAt: now},
}
if err := BatchUpsertStars(d, stars); err != nil {
t.Fatalf("batch upsert stars: %v", err)
}
// Re-insert to confirm ON CONFLICT DO NOTHING.
if err := BatchUpsertStars(d, stars); err != nil {
t.Fatalf("rerun: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM stars`); got != 1 {
t.Errorf("stars count = %d, want 1", got)
}
}
func TestBatchUpsertRepoPages(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
now := time.Now()
pages := []RepoPage{
{DID: "did:plc:alice", Repository: "app", Description: "desc", CreatedAt: now, UpdatedAt: now},
}
if err := BatchUpsertRepoPages(d, pages); err != nil {
t.Fatalf("batch upsert: %v", err)
}
// Update with new description.
pages[0].Description = "new desc"
if err := BatchUpsertRepoPages(d, pages); err != nil {
t.Fatalf("update: %v", err)
}
var desc string
if err := d.QueryRow(`SELECT description FROM repo_pages WHERE did=? AND repository=?`,
"did:plc:alice", "app").Scan(&desc); err != nil {
t.Fatalf("select: %v", err)
}
if desc != "new desc" {
t.Errorf("description = %q, want %q", desc, "new desc")
}
}
func TestBatchUpsertDailyStats(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
stats := []DailyStats{
{DID: "did:plc:alice", Repository: "app", Date: "2026-04-19", PullCount: 5, PushCount: 2},
}
if err := BatchUpsertDailyStats(d, stats); err != nil {
t.Fatalf("upsert: %v", err)
}
stats[0].PullCount = 10
if err := BatchUpsertDailyStats(d, stats); err != nil {
t.Fatalf("update: %v", err)
}
var pull int
if err := d.QueryRow(`SELECT pull_count FROM repository_stats_daily WHERE did=? AND repository=? AND date=?`,
"did:plc:alice", "app", "2026-04-19").Scan(&pull); err != nil {
t.Fatalf("select: %v", err)
}
if pull != 10 {
t.Errorf("pull = %d, want 10", pull)
}
}
func TestBatchUpsertRepositoryAnnotations_DropsStaleKeys(t *testing.T) {
d := setupBatchTestDB(t)
createBatchTestUser(t, d, "did:plc:alice")
rows := []AnnotationRow{
{DID: "did:plc:alice", Repository: "app", Key: "a", Value: "1"},
{DID: "did:plc:alice", Repository: "app", Key: "b", Value: "2"},
}
if err := BatchUpsertRepositoryAnnotations(d, rows); err != nil {
t.Fatalf("initial: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM repository_annotations WHERE did=? AND repository=?`,
"did:plc:alice", "app"); got != 2 {
t.Errorf("initial count = %d, want 2", got)
}
// Second call drops stale key "b".
rows = []AnnotationRow{
{DID: "did:plc:alice", Repository: "app", Key: "a", Value: "1-updated"},
}
if err := BatchUpsertRepositoryAnnotations(d, rows); err != nil {
t.Fatalf("update: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM repository_annotations WHERE did=? AND repository=?`,
"did:plc:alice", "app"); got != 1 {
t.Errorf("after update = %d, want 1", got)
}
var val string
if err := d.QueryRow(`SELECT value FROM repository_annotations WHERE key=? AND did=? AND repository=?`,
"a", "did:plc:alice", "app").Scan(&val); err != nil {
t.Fatalf("select: %v", err)
}
if val != "1-updated" {
t.Errorf("value = %q, want 1-updated", val)
}
}
func TestBatchUpsertCaptainRecords(t *testing.T) {
d := setupBatchTestDB(t)
now := time.Now()
records := []HoldCaptainRecord{
{HoldDID: "did:web:hold1", OwnerDID: "did:plc:alice", Public: true, AllowAllCrew: false, UpdatedAt: now},
}
if err := BatchUpsertCaptainRecords(d, records); err != nil {
t.Fatalf("upsert: %v", err)
}
if got := countRows(t, d, `SELECT COUNT(*) FROM hold_captain_records`); got != 1 {
t.Errorf("count = %d, want 1", got)
}
}
func TestBatchUpsertCrewMembers(t *testing.T) {
d := setupBatchTestDB(t)
members := []CrewMember{
{HoldDID: "did:web:hold1", MemberDID: "did:plc:alice", Rkey: "rkey1", Role: "owner"},
}
if err := BatchUpsertCrewMembers(d, members); err != nil {
t.Fatalf("upsert: %v", err)
}
// Update the rkey: triggers the ON CONFLICT path.
members[0].Rkey = "rkey2"
if err := BatchUpsertCrewMembers(d, members); err != nil {
t.Fatalf("update: %v", err)
}
var rkey string
if err := d.QueryRow(`SELECT rkey FROM hold_crew_members WHERE hold_did=? AND member_did=?`,
"did:web:hold1", "did:plc:alice").Scan(&rkey); err != nil {
t.Fatalf("select: %v", err)
}
if rkey != "rkey2" {
t.Errorf("rkey = %q, want rkey2", rkey)
}
}
func TestBatchEmptySlices(t *testing.T) {
d := setupBatchTestDB(t)
// Every batch function must tolerate an empty input slice without erroring.
if _, err := BatchInsertManifests(d, nil); err != nil {
t.Errorf("manifests: %v", err)
}
if err := BatchInsertLayers(d, nil); err != nil {
t.Errorf("layers: %v", err)
}
if err := BatchInsertManifestReferences(d, nil); err != nil {
t.Errorf("refs: %v", err)
}
if err := BatchUpsertTags(d, nil); err != nil {
t.Errorf("tags: %v", err)
}
if err := BatchUpsertStars(d, nil); err != nil {
t.Errorf("stars: %v", err)
}
if err := BatchUpsertRepoPages(d, nil); err != nil {
t.Errorf("repo pages: %v", err)
}
if err := BatchUpsertDailyStats(d, nil); err != nil {
t.Errorf("daily: %v", err)
}
if err := BatchUpsertRepositoryStats(d, nil); err != nil {
t.Errorf("repo stats: %v", err)
}
if err := BatchUpsertCaptainRecords(d, nil); err != nil {
t.Errorf("captain: %v", err)
}
if err := BatchUpsertCrewMembers(d, nil); err != nil {
t.Errorf("crew: %v", err)
}
if err := BatchUpsertRepositoryAnnotations(d, nil); err != nil {
t.Errorf("annotations: %v", err)
}
}