test: raise core-package unit coverage to 90% (#98)

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>
This commit was merged in pull request #98.
This commit is contained in:
2026-07-03 14:31:24 +10:00
committed by BenVincent
parent 1b585af14e
commit a1ba86e76b
26 changed files with 3555 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
package cache
import (
"context"
"os"
"testing"
"time"
"git.unkin.net/unkin/artifactapi/internal/testsupport"
)
var testRedis *Redis
func TestMain(m *testing.M) {
ctx := context.Background()
url, terminate, err := testsupport.StartRedis(ctx)
if err != nil {
os.Exit(m.Run())
}
r, err := NewRedis(url)
if err != nil {
terminate()
panic(err)
}
testRedis = r
code := m.Run()
r.Close()
terminate()
if code != 0 {
os.Exit(code)
}
}
func requireRedis(t *testing.T) {
t.Helper()
if testRedis == nil {
t.Skip("Docker unavailable; skipping cache integration test")
}
}
func TestNewRedisInvalid(t *testing.T) {
if _, err := NewRedis("://bad-url"); err == nil {
t.Error("expected error for invalid redis URL")
}
}
func TestTTL(t *testing.T) {
requireRedis(t)
ctx := context.Background()
if fresh, _ := testRedis.CheckTTL(ctx, "r", "missing"); fresh {
t.Error("missing key should not be fresh")
}
if err := testRedis.SetTTL(ctx, "r", "p", time.Minute); err != nil {
t.Fatal(err)
}
if fresh, err := testRedis.CheckTTL(ctx, "r", "p"); err != nil || !fresh {
t.Errorf("expected fresh after SetTTL: %v %v", fresh, err)
}
}
func TestLock(t *testing.T) {
requireRedis(t)
ctx := context.Background()
ok, err := testRedis.AcquireLock(ctx, "r", "lockpath", time.Minute)
if err != nil || !ok {
t.Fatalf("first acquire should succeed: %v %v", ok, err)
}
if ok, _ := testRedis.AcquireLock(ctx, "r", "lockpath", time.Minute); ok {
t.Error("second acquire should fail while held")
}
if err := testRedis.ReleaseLock(ctx, "r", "lockpath"); err != nil {
t.Fatal(err)
}
if ok, _ := testRedis.AcquireLock(ctx, "r", "lockpath", time.Minute); !ok {
t.Error("acquire should succeed after release")
}
}
func TestETagAndToken(t *testing.T) {
requireRedis(t)
ctx := context.Background()
if v, _ := testRedis.GetETag(ctx, "r", "missing"); v != "" {
t.Error("missing etag should be empty")
}
testRedis.SetETag(ctx, "r", "p", `"abc"`, time.Minute)
if v, _ := testRedis.GetETag(ctx, "r", "p"); v != `"abc"` {
t.Errorf("etag = %q", v)
}
if v, _ := testRedis.GetToken(ctx, "missing"); v != "" {
t.Error("missing token should be empty")
}
testRedis.SetToken(ctx, "key", "tok", time.Minute)
if v, _ := testRedis.GetToken(ctx, "key"); v != "tok" {
t.Errorf("token = %q", v)
}
}
func TestCircuit(t *testing.T) {
requireRedis(t)
ctx := context.Background()
if n, _ := testRedis.GetCircuitFailures(ctx, "cr"); n != 0 {
t.Errorf("initial failures = %d", n)
}
n1, err := testRedis.IncrCircuitFailure(ctx, "cr", time.Minute)
if err != nil || n1 != 1 {
t.Fatalf("first incr = %d %v", n1, err)
}
n2, _ := testRedis.IncrCircuitFailure(ctx, "cr", time.Minute)
if n2 != 2 {
t.Errorf("second incr = %d", n2)
}
if n, _ := testRedis.GetCircuitFailures(ctx, "cr"); n != 2 {
t.Errorf("get failures = %d", n)
}
testRedis.ResetCircuit(ctx, "cr")
if n, _ := testRedis.GetCircuitFailures(ctx, "cr"); n != 0 {
t.Errorf("failures after reset = %d", n)
}
}
func TestFlushRemote(t *testing.T) {
requireRedis(t)
ctx := context.Background()
testRedis.SetTTL(ctx, "flushme", "a", time.Hour)
testRedis.SetETag(ctx, "flushme", "a", "x", time.Hour)
if err := testRedis.FlushRemote(ctx, "flushme"); err != nil {
t.Fatal(err)
}
if fresh, _ := testRedis.CheckTTL(ctx, "flushme", "a"); fresh {
t.Error("expected keys flushed")
}
}