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
+59
View File
@@ -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()
}