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:
@@ -0,0 +1,101 @@
|
||||
// Package testsupport starts throwaway backing containers (Postgres, Redis,
|
||||
// MinIO) for integration-style unit tests. It is only ever imported from
|
||||
// *_test.go files, so it never reaches the production binary. Each Start*
|
||||
// function returns a connection detail plus a terminate func; callers wire
|
||||
// them up in a TestMain and skip the package's tests when Docker is absent.
|
||||
package testsupport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
tcredis "github.com/testcontainers/testcontainers-go/modules/redis"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The Ryuk reaper container cannot start in this environment; each Start*
|
||||
// returns an explicit terminate func for cleanup instead.
|
||||
if _, ok := os.LookupEnv("TESTCONTAINERS_RYUK_DISABLED"); !ok {
|
||||
os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
|
||||
}
|
||||
}
|
||||
|
||||
// StartPostgres launches postgres:17-alpine and returns its DSN.
|
||||
func StartPostgres(ctx context.Context) (dsn string, terminate func(), err error) {
|
||||
c, err := tcpostgres.Run(ctx,
|
||||
"postgres:17-alpine",
|
||||
tcpostgres.WithDatabase("artifacts"),
|
||||
tcpostgres.WithUsername("artifacts"),
|
||||
tcpostgres.WithPassword("artifacts123"),
|
||||
testcontainers.WithWaitStrategy(
|
||||
// Postgres opens the port, runs init scripts, then restarts, so wait
|
||||
// for the readiness log to appear twice to avoid connection resets.
|
||||
wait.ForLog("database system is ready to accept connections").
|
||||
WithOccurrence(2).
|
||||
WithStartupTimeout(60*time.Second),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
host, _ := c.Host(ctx)
|
||||
port, _ := c.MappedPort(ctx, "5432/tcp")
|
||||
dsn = fmt.Sprintf("postgres://artifacts:artifacts123@%s:%s/artifacts?sslmode=disable", host, port.Port())
|
||||
return dsn, func() { _ = c.Terminate(ctx) }, nil
|
||||
}
|
||||
|
||||
// StartRedis launches redis:7-alpine and returns its URL.
|
||||
func StartRedis(ctx context.Context) (url string, terminate func(), err error) {
|
||||
c, err := tcredis.Run(ctx,
|
||||
"redis:7-alpine",
|
||||
testcontainers.WithWaitStrategy(
|
||||
wait.ForListeningPort("6379/tcp").WithStartupTimeout(60*time.Second),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
host, _ := c.Host(ctx)
|
||||
port, _ := c.MappedPort(ctx, "6379/tcp")
|
||||
url = fmt.Sprintf("redis://%s:%s", host, port.Port())
|
||||
return url, func() { _ = c.Terminate(ctx) }, nil
|
||||
}
|
||||
|
||||
// MinioConn holds MinIO connection details.
|
||||
type MinioConn struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// StartMinio launches minio and returns its connection details.
|
||||
func StartMinio(ctx context.Context) (conn MinioConn, terminate func(), err error) {
|
||||
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: testcontainers.ContainerRequest{
|
||||
Image: "minio/minio:latest",
|
||||
ExposedPorts: []string{"9000/tcp"},
|
||||
Cmd: []string{"server", "/data"},
|
||||
Env: map[string]string{
|
||||
"MINIO_ROOT_USER": "minioadmin",
|
||||
"MINIO_ROOT_PASSWORD": "minioadmin",
|
||||
},
|
||||
WaitingFor: wait.ForHTTP("/minio/health/ready").WithPort("9000/tcp").WithStartupTimeout(60 * time.Second),
|
||||
},
|
||||
Started: true,
|
||||
})
|
||||
if err != nil {
|
||||
return MinioConn{}, nil, err
|
||||
}
|
||||
host, _ := c.Host(ctx)
|
||||
port, _ := c.MappedPort(ctx, "9000/tcp")
|
||||
return MinioConn{
|
||||
Endpoint: fmt.Sprintf("%s:%s", host, port.Port()),
|
||||
AccessKey: "minioadmin",
|
||||
SecretKey: "minioadmin",
|
||||
}, func() { _ = c.Terminate(ctx) }, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package testsupport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// MinimalRPM builds a valid-enough RPM package in pure Go (no committed binary
|
||||
// fixture, no external rpmbuild). It carries just the header tags the provider
|
||||
// reads: name/version/release/arch plus a single self Provides entry, which is
|
||||
// enough for cavaliergopher/rpm to parse and for repodata generation.
|
||||
func MinimalRPM(name, version, release, arch string) []byte {
|
||||
type tag struct {
|
||||
id, typ, count uint32
|
||||
data []byte
|
||||
}
|
||||
cstr := func(s string) []byte { return append([]byte(s), 0) }
|
||||
tags := []tag{
|
||||
{1000, 6, 1, cstr(name)}, // RPMTAG_NAME (STRING)
|
||||
{1001, 6, 1, cstr(version)}, // RPMTAG_VERSION
|
||||
{1002, 6, 1, cstr(release)}, // RPMTAG_RELEASE
|
||||
{1022, 6, 1, cstr(arch)}, // RPMTAG_ARCH
|
||||
{1047, 8, 1, cstr(name)}, // RPMTAG_PROVIDENAME (STRING_ARRAY)
|
||||
{1112, 4, 1, []byte{0, 0, 0, 0}}, // RPMTAG_PROVIDEFLAGS (INT32)
|
||||
{1113, 8, 1, cstr(version)}, // RPMTAG_PROVIDEVERSION (STRING_ARRAY)
|
||||
}
|
||||
|
||||
buildHeader := func(entries []tag) []byte {
|
||||
var index, store bytes.Buffer
|
||||
for _, e := range entries {
|
||||
off := uint32(store.Len())
|
||||
for _, v := range []uint32{e.id, e.typ, off, e.count} {
|
||||
binary.Write(&index, binary.BigEndian, v)
|
||||
}
|
||||
store.Write(e.data)
|
||||
}
|
||||
var b bytes.Buffer
|
||||
b.Write([]byte{0x8e, 0xad, 0xe8, 0x01, 0, 0, 0, 0}) // header magic + reserved
|
||||
binary.Write(&b, binary.BigEndian, uint32(len(entries)))
|
||||
binary.Write(&b, binary.BigEndian, uint32(store.Len()))
|
||||
b.Write(index.Bytes())
|
||||
b.Write(store.Bytes())
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
lead := make([]byte, 96)
|
||||
copy(lead[0:4], []byte{0xed, 0xab, 0xee, 0xdb}) // lead magic
|
||||
lead[4] = 3 // major version
|
||||
binary.BigEndian.PutUint16(lead[8:10], 1) // archnum
|
||||
copy(lead[10:76], name) // name (66 bytes, null-padded)
|
||||
binary.BigEndian.PutUint16(lead[76:78], 1) // osnum
|
||||
binary.BigEndian.PutUint16(lead[78:80], 5) // signature type
|
||||
|
||||
var out bytes.Buffer
|
||||
out.Write(lead)
|
||||
out.Write(buildHeader(nil)) // empty signature header (16 bytes, 8-aligned)
|
||||
out.Write(buildHeader(tags))
|
||||
return out.Bytes()
|
||||
}
|
||||
Reference in New Issue
Block a user