test: full-stack server integration test
Drives the whole HTTP API against real Postgres/Redis/MinIO + a mock upstream: health, remote CRUD, proxy miss/hit, local upload/download, virtual helm merge, objects, stats, probe. Exercises server + api/v1 + api/v2 + proxy + virtual engines together.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/config"
|
||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||
)
|
||||
|
||||
var (
|
||||
testTS *httptest.Server // the artifactapi router
|
||||
upstream *httptest.Server // mock upstream the proxy fetches from
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := context.Background()
|
||||
|
||||
dsn, termPG, err := testsupport.StartPostgres(ctx)
|
||||
if err != nil {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
defer termPG()
|
||||
redisURL, termRedis, err := testsupport.StartRedis(ctx)
|
||||
if err != nil {
|
||||
termPG()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
defer termRedis()
|
||||
minio, termMinio, err := testsupport.StartMinio(ctx)
|
||||
if err != nil {
|
||||
termPG()
|
||||
termRedis()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
defer termMinio()
|
||||
|
||||
u, _ := url.Parse(dsn)
|
||||
port, _ := strconv.Atoi(u.Port())
|
||||
cfg := &config.Config{
|
||||
ListenAddr: ":0",
|
||||
DBHost: u.Hostname(),
|
||||
DBPort: port,
|
||||
DBUser: "artifacts",
|
||||
DBPass: "artifacts123",
|
||||
DBName: "artifacts",
|
||||
DBSSL: "disable",
|
||||
RedisURL: redisURL,
|
||||
S3Endpoint: minio.Endpoint,
|
||||
S3AccessKey: minio.AccessKey,
|
||||
S3SecretKey: minio.SecretKey,
|
||||
S3Bucket: "server-test",
|
||||
}
|
||||
|
||||
srv, err := New(cfg, "test-version")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testTS = httptest.NewServer(srv.router)
|
||||
upstream = httptest.NewServer(http.HandlerFunc(mockUpstream))
|
||||
|
||||
code := m.Run()
|
||||
|
||||
testTS.Close()
|
||||
upstream.Close()
|
||||
termMinio()
|
||||
termRedis()
|
||||
termPG()
|
||||
if code != 0 {
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
func mockUpstream(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/data/file.bin":
|
||||
w.Write([]byte("upstream blob payload"))
|
||||
case "/helm-a/index.yaml":
|
||||
w.Write([]byte("apiVersion: v1\nentries:\n alpha:\n - name: alpha\n version: 1.0.0\n urls: [charts/alpha-1.0.0.tgz]\n"))
|
||||
case "/helm-b/index.yaml":
|
||||
w.Write([]byte("apiVersion: v1\nentries:\n beta:\n - name: beta\n version: 2.0.0\n urls: [charts/beta-2.0.0.tgz]\n"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func requireStack(t *testing.T) {
|
||||
t.Helper()
|
||||
if testTS == nil {
|
||||
t.Skip("Docker unavailable; skipping server integration test")
|
||||
}
|
||||
}
|
||||
|
||||
func req(t *testing.T, method, path string, body string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
var r io.Reader
|
||||
if body != "" {
|
||||
r = strings.NewReader(body)
|
||||
}
|
||||
rq, _ := http.NewRequest(method, testTS.URL+path, r)
|
||||
if body != "" {
|
||||
rq.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(rq)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", method, path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return resp, b
|
||||
}
|
||||
|
||||
func TestServerHealthAndRoot(t *testing.T) {
|
||||
requireStack(t)
|
||||
if resp, _ := req(t, "GET", "/health", ""); resp.StatusCode != 200 {
|
||||
t.Errorf("health: %d", resp.StatusCode)
|
||||
}
|
||||
if resp, b := req(t, "GET", "/", ""); resp.StatusCode != 200 || !strings.Contains(string(b), "test-version") {
|
||||
t.Errorf("root: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
if resp, _ := req(t, "GET", "/api/v2/health", ""); resp.StatusCode != 200 {
|
||||
t.Errorf("health v2: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerRemoteAndProxy(t *testing.T) {
|
||||
requireStack(t)
|
||||
create := fmt.Sprintf(`{"name":"srv-remote","package_type":"generic","repo_type":"remote","base_url":%q,"stale_on_error":true}`, upstream.URL)
|
||||
if resp, b := req(t, "POST", "/api/v2/remotes", create); resp.StatusCode != 201 {
|
||||
t.Fatalf("create remote: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
defer req(t, "DELETE", "/api/v2/remotes/srv-remote", "")
|
||||
|
||||
if resp, _ := req(t, "GET", "/api/v2/remotes/srv-remote", ""); resp.StatusCode != 200 {
|
||||
t.Errorf("get remote: %d", resp.StatusCode)
|
||||
}
|
||||
if resp, _ := req(t, "GET", "/api/v2/remotes", ""); resp.StatusCode != 200 {
|
||||
t.Errorf("list remotes: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Proxy fetch: miss then hit.
|
||||
resp, b := req(t, "GET", "/api/v1/remote/srv-remote/data/file.bin", "")
|
||||
if resp.StatusCode != 200 || string(b) != "upstream blob payload" {
|
||||
t.Fatalf("proxy miss: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
if src := resp.Header.Get("X-Artifact-Source"); src != "remote" {
|
||||
t.Errorf("expected remote source, got %q", src)
|
||||
}
|
||||
resp, _ = req(t, "GET", "/api/v1/remote/srv-remote/data/file.bin", "")
|
||||
if resp.Header.Get("X-Artifact-Source") != "cache" {
|
||||
t.Errorf("second fetch should be cache: %q", resp.Header.Get("X-Artifact-Source"))
|
||||
}
|
||||
|
||||
// Objects listing + stats now that we have an artifact.
|
||||
if resp, _ := req(t, "GET", "/api/v2/remotes/srv-remote/objects", ""); resp.StatusCode != 200 {
|
||||
t.Errorf("objects: %d", resp.StatusCode)
|
||||
}
|
||||
if resp, _ := req(t, "GET", "/api/v2/stats", ""); resp.StatusCode != 200 {
|
||||
t.Errorf("stats: %d", resp.StatusCode)
|
||||
}
|
||||
for _, p := range []string{"/api/v2/stats/top-remotes", "/api/v2/stats/top-files-by-hits", "/api/v2/stats/top-files-by-bandwidth"} {
|
||||
if resp, _ := req(t, "GET", p, ""); resp.StatusCode != 200 {
|
||||
t.Errorf("%s: %d", p, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerLocalUpload(t *testing.T) {
|
||||
requireStack(t)
|
||||
if resp, b := req(t, "POST", "/api/v2/remotes", `{"name":"srv-local","package_type":"generic","repo_type":"local"}`); resp.StatusCode != 201 {
|
||||
t.Fatalf("create local: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
defer req(t, "DELETE", "/api/v2/remotes/srv-local", "")
|
||||
|
||||
rq, _ := http.NewRequest("PUT", testTS.URL+"/api/v2/remotes/srv-local/files/dir/hello.bin", strings.NewReader("local payload"))
|
||||
resp, err := http.DefaultClient.Do(rq)
|
||||
if err != nil || resp.StatusCode != 201 {
|
||||
t.Fatalf("upload: %v %d", err, resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
resp, b := req(t, "GET", "/api/v1/local/srv-local/dir/hello.bin", "")
|
||||
if resp.StatusCode != 200 || string(b) != "local payload" {
|
||||
t.Errorf("download local: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerVirtualMerge(t *testing.T) {
|
||||
requireStack(t)
|
||||
for _, m := range []string{"a", "b"} {
|
||||
body := fmt.Sprintf(`{"name":"srv-helm-%s","package_type":"helm","repo_type":"remote","base_url":"%s/helm-%s","stale_on_error":true}`, m, upstream.URL, m)
|
||||
if resp, b := req(t, "POST", "/api/v2/remotes", body); resp.StatusCode != 201 {
|
||||
t.Fatalf("create helm-%s: %d %s", m, resp.StatusCode, b)
|
||||
}
|
||||
defer req(t, "DELETE", "/api/v2/remotes/srv-helm-"+m, "")
|
||||
}
|
||||
if resp, b := req(t, "POST", "/api/v2/virtuals", `{"name":"srv-vh","package_type":"helm","members":["srv-helm-a","srv-helm-b"]}`); resp.StatusCode != 201 {
|
||||
t.Fatalf("create virtual: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
defer req(t, "DELETE", "/api/v2/virtuals/srv-vh", "")
|
||||
|
||||
resp, b := req(t, "GET", "/api/v1/virtual/srv-vh/index.yaml", "")
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("virtual fetch: %d %s", resp.StatusCode, b)
|
||||
}
|
||||
s := string(b)
|
||||
if !strings.Contains(s, "alpha") || !strings.Contains(s, "beta") {
|
||||
t.Errorf("merged index missing charts: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerProbe(t *testing.T) {
|
||||
requireStack(t)
|
||||
body := fmt.Sprintf(`{"package_type":"generic","base_url":%q,"path":"data/file.bin"}`, upstream.URL)
|
||||
resp, _ := req(t, "POST", "/api/v2/probe", body)
|
||||
// Probe should reach the mock upstream and report reachable (200) or a
|
||||
// structured result; either way not a server error.
|
||||
if resp.StatusCode >= 500 {
|
||||
t.Errorf("probe server error: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerNotFound(t *testing.T) {
|
||||
requireStack(t)
|
||||
if resp, _ := req(t, "GET", "/api/v2/remotes/does-not-exist", ""); resp.StatusCode != 404 {
|
||||
t.Errorf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
if resp, _ := req(t, "GET", "/api/v1/remote/nope/x", ""); resp.StatusCode != 404 {
|
||||
t.Errorf("expected 404 for unknown remote, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user