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,145 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func testServer(t *testing.T, h http.HandlerFunc) *Client {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(h)
|
||||
t.Cleanup(srv.Close)
|
||||
return New(srv.URL)
|
||||
}
|
||||
|
||||
func TestRemotesRoundTrip(t *testing.T) {
|
||||
var gotMethod, gotPath string
|
||||
c := testServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod, gotPath = r.Method, r.URL.Path
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/remotes":
|
||||
w.Write([]byte(`[{"name":"a"},{"name":"b"}]`))
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/remotes/a":
|
||||
w.Write([]byte(`{"name":"a"}`))
|
||||
case r.Method == http.MethodDelete:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(`{"name":"a"}`))
|
||||
}
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
remotes, err := c.ListRemotes(ctx)
|
||||
if err != nil || len(remotes) != 2 {
|
||||
t.Fatalf("ListRemotes: %v %v", remotes, err)
|
||||
}
|
||||
if r, err := c.GetRemote(ctx, "a"); err != nil || r.Name != "a" {
|
||||
t.Fatalf("GetRemote: %v %v", r, err)
|
||||
}
|
||||
if err := c.CreateRemote(ctx, &models.Remote{Name: "a", PackageType: models.PackageGeneric}); err != nil {
|
||||
t.Fatalf("CreateRemote: %v", err)
|
||||
}
|
||||
if err := c.UpdateRemote(ctx, &models.Remote{Name: "a"}); err != nil {
|
||||
t.Fatalf("UpdateRemote: %v", err)
|
||||
}
|
||||
if err := c.DeleteRemote(ctx, "a"); err != nil {
|
||||
t.Fatalf("DeleteRemote: %v", err)
|
||||
}
|
||||
if gotMethod != http.MethodDelete || gotPath != "/api/v2/remotes/a" {
|
||||
t.Errorf("last call = %s %s", gotMethod, gotPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualsRoundTrip(t *testing.T) {
|
||||
c := testServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/virtuals"):
|
||||
w.Write([]byte(`[{"name":"v"}]`))
|
||||
case r.Method == http.MethodGet:
|
||||
w.Write([]byte(`{"name":"v"}`))
|
||||
case r.Method == http.MethodDelete:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(`{"name":"v"}`))
|
||||
}
|
||||
})
|
||||
ctx := context.Background()
|
||||
if vs, err := c.ListVirtuals(ctx); err != nil || len(vs) != 1 {
|
||||
t.Fatalf("ListVirtuals: %v %v", vs, err)
|
||||
}
|
||||
if v, err := c.GetVirtual(ctx, "v"); err != nil || v.Name != "v" {
|
||||
t.Fatalf("GetVirtual: %v %v", v, err)
|
||||
}
|
||||
if err := c.CreateVirtual(ctx, &models.Virtual{Name: "v"}); err != nil {
|
||||
t.Fatalf("CreateVirtual: %v", err)
|
||||
}
|
||||
if err := c.UpdateVirtual(ctx, &models.Virtual{Name: "v"}); err != nil {
|
||||
t.Fatalf("UpdateVirtual: %v", err)
|
||||
}
|
||||
if err := c.DeleteVirtual(ctx, "v"); err != nil {
|
||||
t.Fatalf("DeleteVirtual: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsHealthObjects(t *testing.T) {
|
||||
c := testServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/stats"):
|
||||
w.Write([]byte(`{"total_remotes":3}`))
|
||||
case strings.HasSuffix(r.URL.Path, "/health"):
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
case r.Method == http.MethodDelete:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.Write([]byte(`[{"path":"p"}]`))
|
||||
}
|
||||
})
|
||||
ctx := context.Background()
|
||||
if _, err := c.Stats(ctx); err != nil {
|
||||
t.Fatalf("Stats: %v", err)
|
||||
}
|
||||
if _, err := c.Health(ctx); err != nil {
|
||||
t.Fatalf("Health: %v", err)
|
||||
}
|
||||
if objs, err := c.ListObjects(ctx, "r", 1, 50); err != nil || len(objs) != 1 {
|
||||
t.Fatalf("ListObjects: %v %v", objs, err)
|
||||
}
|
||||
if err := c.EvictObject(ctx, "r", "some/path"); err != nil {
|
||||
t.Fatalf("EvictObject: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorResponses(t *testing.T) {
|
||||
c := testServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
})
|
||||
ctx := context.Background()
|
||||
_, err := c.GetRemote(ctx, "x")
|
||||
if err == nil || !strings.Contains(err.Error(), "api error 500") {
|
||||
t.Errorf("expected api error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError(t *testing.T) {
|
||||
c := testServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`not json`))
|
||||
})
|
||||
if _, err := c.ListRemotes(context.Background()); err == nil || !strings.Contains(err.Error(), "decode") {
|
||||
t.Errorf("expected decode error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestError(t *testing.T) {
|
||||
// Invalid base URL triggers request construction failure.
|
||||
c := New("http://[::1]:namedport")
|
||||
if err := c.DeleteRemote(context.Background(), "x"); err == nil {
|
||||
t.Error("expected request error for invalid URL")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRepoType(t *testing.T) {
|
||||
if RepoTypeRemote.String() != "remote" || RepoTypeLocal.String() != "local" {
|
||||
t.Error("RepoType.String")
|
||||
}
|
||||
if !RepoTypeRemote.Valid() || RepoType("bogus").Valid() {
|
||||
t.Error("RepoType.Valid")
|
||||
}
|
||||
if rt, err := ParseRepoType("local"); err != nil || rt != RepoTypeLocal {
|
||||
t.Errorf("ParseRepoType(local) = %v %v", rt, err)
|
||||
}
|
||||
if _, err := ParseRepoType("nope"); err == nil {
|
||||
t.Error("ParseRepoType should reject unknown")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user