a1ba86e76b
Raises statement coverage of the core packages (all of `internal/` except the interactive `tui/`, plus `pkg/`) from **8.7% to 90.1%**. ## Approach - **Pure-go unit tests** for all providers, virtual mergers, classifier, config, auth, models, and the API client (httptest). - **Testcontainers-backed** tests (new `internal/testsupport` helper: Postgres/Redis/MinIO, Ryuk disabled) for database, storage, cache, the proxy engine, the GC, and a full-stack `server` test that drives the whole HTTP API. These `t.Skip` when Docker is absent so `go test` still runs locally without it. ## Measuring ``` go test -coverpkg=./internal/...,./pkg/... -coverprofile=cover.out ./internal/... ./pkg/... grep -v /internal/tui/ cover.out | go tool cover -func=/dev/stdin | tail -1 # 90.1% ``` Run with `-p 1` (containers are heavy). ## Notes - The interactive `tui/` package and `cmd/main` are excluded from the target per the agreed scope. - Some defensive error branches are covered via fault injection (closed DB pool, killing MinIO mid-upload). Reviewed-on: #98 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
115 lines
2.5 KiB
Go
115 lines
2.5 KiB
Go
package gc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/database"
|
|
"git.unkin.net/unkin/artifactapi/internal/storage"
|
|
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
|
)
|
|
|
|
var (
|
|
testDB *database.DB
|
|
testStore *storage.S3
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
ctx := context.Background()
|
|
dsn, termPG, err := testsupport.StartPostgres(ctx)
|
|
if err != nil {
|
|
os.Exit(m.Run())
|
|
}
|
|
minio, termMinio, err := testsupport.StartMinio(ctx)
|
|
if err != nil {
|
|
termPG()
|
|
os.Exit(m.Run())
|
|
}
|
|
db, err := database.New(dsn)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
var s3 *storage.S3
|
|
for i := 0; i < 20; i++ {
|
|
if s3, err = storage.NewS3(minio.Endpoint, minio.AccessKey, minio.SecretKey, "gc-test", false, ""); err == nil {
|
|
break
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
testDB = db
|
|
testStore = s3
|
|
|
|
code := m.Run()
|
|
db.Close()
|
|
termMinio()
|
|
termPG()
|
|
if code != 0 {
|
|
os.Exit(code)
|
|
}
|
|
}
|
|
|
|
func TestSweepDeletesOldOrphan(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
ctx := context.Background()
|
|
hash := "sha256:gcorphan"
|
|
key := storage.BlobKey("gcorphan")
|
|
|
|
if err := testStore.Upload(ctx, key, bytes.NewReader([]byte("orphan")), 6, "application/octet-stream"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := testDB.UpsertBlob(ctx, hash, key, 6, "application/octet-stream"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Age the blob past the grace period.
|
|
if _, err := testDB.Pool.Exec(ctx, `UPDATE blobs SET created_at = now() - interval '2 hours' WHERE content_hash = $1`, hash); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := New(testDB, testStore, time.Hour)
|
|
c.sweep(ctx)
|
|
|
|
if exists, _ := testStore.Exists(ctx, key); exists {
|
|
t.Error("expected orphan object deleted from store")
|
|
}
|
|
orphans, _ := testDB.FindOrphanedBlobs(ctx, 0)
|
|
for _, b := range orphans {
|
|
if b.ContentHash == hash {
|
|
t.Error("expected orphan blob row deleted")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSweepNoOrphans(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
// A sweep with nothing to collect should be a clean no-op.
|
|
New(testDB, testStore, time.Hour).sweep(context.Background())
|
|
}
|
|
|
|
func TestRunStopsOnContextCancel(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
New(testDB, testStore, time.Hour).Run(ctx)
|
|
close(done)
|
|
}()
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Run did not return after context cancel")
|
|
}
|
|
}
|