298 lines
6.7 KiB
Go
298 lines
6.7 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/any-hub/any-hub/internal/cache"
|
|
"github.com/any-hub/any-hub/internal/config"
|
|
"github.com/any-hub/any-hub/internal/proxy"
|
|
"github.com/any-hub/any-hub/internal/server"
|
|
)
|
|
|
|
func TestDockerHookEmitsLogFields(t *testing.T) {
|
|
stub := newCacheFlowStub(t, dockerManifestPath)
|
|
defer stub.Close()
|
|
|
|
env := newHookTestEnv(t, config.Config{
|
|
Global: config.GlobalConfig{
|
|
ListenPort: 5300,
|
|
CacheTTL: config.Duration(time.Minute),
|
|
StoragePath: t.TempDir(),
|
|
},
|
|
Hubs: []config.HubConfig{
|
|
{
|
|
Name: "docker",
|
|
Domain: "docker.hook.local",
|
|
Type: "docker",
|
|
Upstream: stub.URL,
|
|
},
|
|
},
|
|
})
|
|
defer env.Close()
|
|
|
|
assertCacheMissThenHit(t, env, "docker.hook.local", dockerManifestPath)
|
|
env.AssertLogContains(t, `"module_key":"docker"`)
|
|
env.AssertLogContains(t, `"cache_hit":false`)
|
|
env.AssertLogContains(t, `"cache_hit":true`)
|
|
}
|
|
|
|
func TestNPMHookEmitsLogFields(t *testing.T) {
|
|
stub := newUpstreamStub(t, upstreamNPM)
|
|
defer stub.Close()
|
|
|
|
env := newHookTestEnv(t, config.Config{
|
|
Global: config.GlobalConfig{
|
|
ListenPort: 5310,
|
|
CacheTTL: config.Duration(time.Minute),
|
|
StoragePath: t.TempDir(),
|
|
},
|
|
Hubs: []config.HubConfig{
|
|
{
|
|
Name: "npm",
|
|
Domain: "npm.hook.local",
|
|
Type: "npm",
|
|
Upstream: stub.URL,
|
|
},
|
|
},
|
|
})
|
|
defer env.Close()
|
|
|
|
assertCacheMissThenHit(t, env, "npm.hook.local", "/lodash")
|
|
env.AssertLogContains(t, `"module_key":"npm"`)
|
|
}
|
|
|
|
func TestPyPIHookEmitsLogFields(t *testing.T) {
|
|
stub := newPyPIStub(t)
|
|
defer stub.Close()
|
|
|
|
env := newHookTestEnv(t, config.Config{
|
|
Global: config.GlobalConfig{
|
|
ListenPort: 5320,
|
|
CacheTTL: config.Duration(time.Minute),
|
|
StoragePath: t.TempDir(),
|
|
},
|
|
Hubs: []config.HubConfig{
|
|
{
|
|
Name: "pypi",
|
|
Domain: "pypi.hook.local",
|
|
Type: "pypi",
|
|
Upstream: stub.URL,
|
|
},
|
|
},
|
|
})
|
|
defer env.Close()
|
|
|
|
assertCacheMissThenHit(t, env, "pypi.hook.local", "/simple/pkg/")
|
|
env.AssertLogContains(t, `"module_key":"pypi"`)
|
|
}
|
|
|
|
func TestComposerHookEmitsLogFields(t *testing.T) {
|
|
stub := newComposerStub(t)
|
|
defer stub.Close()
|
|
|
|
env := newHookTestEnv(t, config.Config{
|
|
Global: config.GlobalConfig{
|
|
ListenPort: 5330,
|
|
CacheTTL: config.Duration(time.Minute),
|
|
StoragePath: t.TempDir(),
|
|
},
|
|
Hubs: []config.HubConfig{
|
|
{
|
|
Name: "composer",
|
|
Domain: "composer.hook.local",
|
|
Type: "composer",
|
|
Upstream: stub.URL,
|
|
},
|
|
},
|
|
})
|
|
defer env.Close()
|
|
|
|
assertCacheMissThenHit(t, env, "composer.hook.local", "/p2/example/package.json")
|
|
env.AssertLogContains(t, `"module_key":"composer"`)
|
|
}
|
|
|
|
func TestGoHookEmitsLogFields(t *testing.T) {
|
|
stub := newGoStub(t)
|
|
defer stub.Close()
|
|
|
|
env := newHookTestEnv(t, config.Config{
|
|
Global: config.GlobalConfig{
|
|
ListenPort: 5340,
|
|
CacheTTL: config.Duration(time.Minute),
|
|
StoragePath: t.TempDir(),
|
|
},
|
|
Hubs: []config.HubConfig{
|
|
{
|
|
Name: "gomod",
|
|
Domain: "go.hook.local",
|
|
Type: "go",
|
|
Upstream: stub.URL,
|
|
},
|
|
},
|
|
})
|
|
defer env.Close()
|
|
|
|
assertCacheMissThenHit(t, env, "go.hook.local", goZipPath)
|
|
env.AssertLogContains(t, `"module_key":"go"`)
|
|
}
|
|
|
|
func assertCacheMissThenHit(t *testing.T, env hookTestEnv, host, path string) {
|
|
t.Helper()
|
|
resp := env.DoRequest(t, host, path)
|
|
if resp.StatusCode != fiber.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
t.Fatalf("expected 200 for %s%s, got %d body=%s", host, path, resp.StatusCode, string(body))
|
|
}
|
|
if hit := resp.Header.Get("X-Any-Hub-Cache-Hit"); hit != "false" {
|
|
resp.Body.Close()
|
|
t.Fatalf("expected cache miss header, got %s", hit)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
resp2 := env.DoRequest(t, host, path)
|
|
if resp2.Header.Get("X-Any-Hub-Cache-Hit") != "true" {
|
|
resp2.Body.Close()
|
|
t.Fatalf("expected cache hit header on second request, got %s", resp2.Header.Get("X-Any-Hub-Cache-Hit"))
|
|
}
|
|
resp2.Body.Close()
|
|
}
|
|
|
|
type hookTestEnv struct {
|
|
app *fiber.App
|
|
logs *bytes.Buffer
|
|
}
|
|
|
|
func newHookTestEnv(t *testing.T, cfg config.Config) hookTestEnv {
|
|
t.Helper()
|
|
|
|
registry, err := server.NewHubRegistry(&cfg)
|
|
if err != nil {
|
|
t.Fatalf("registry error: %v", err)
|
|
}
|
|
|
|
logger := logrus.New()
|
|
buf := &bytes.Buffer{}
|
|
logger.SetFormatter(&logrus.JSONFormatter{})
|
|
logger.SetOutput(buf)
|
|
|
|
store, err := cache.NewStore(cfg.Global.StoragePath)
|
|
if err != nil {
|
|
t.Fatalf("store error: %v", err)
|
|
}
|
|
handler := proxy.NewHandler(server.NewUpstreamClient(&cfg), logger, store)
|
|
|
|
app, err := server.NewApp(server.AppOptions{
|
|
Logger: logger,
|
|
Registry: registry,
|
|
Proxy: handler,
|
|
ListenPort: cfg.Global.ListenPort,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("app error: %v", err)
|
|
}
|
|
return hookTestEnv{app: app, logs: buf}
|
|
}
|
|
|
|
func (env hookTestEnv) DoRequest(t *testing.T, host, path string) *http.Response {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "http://"+host+path, nil)
|
|
req.Host = host
|
|
resp, err := env.app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test error: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func (env hookTestEnv) AssertLogContains(t *testing.T, substr string) {
|
|
t.Helper()
|
|
if !strings.Contains(env.logs.String(), substr) {
|
|
t.Fatalf("expected logs to contain %s, got %s", substr, env.logs.String())
|
|
}
|
|
}
|
|
|
|
func (env hookTestEnv) Close() {
|
|
_ = env.app.Shutdown()
|
|
}
|
|
|
|
const (
|
|
goZipPath = "/mod.example/@v/v1.0.0.zip"
|
|
goInfoPath = "/mod.example/@v/v1.0.0.info"
|
|
)
|
|
|
|
type goStub struct {
|
|
server *http.Server
|
|
listener net.Listener
|
|
URL string
|
|
|
|
mu sync.Mutex
|
|
hits map[string]int
|
|
}
|
|
|
|
func newGoStub(t *testing.T) *goStub {
|
|
t.Helper()
|
|
stub := &goStub{hits: make(map[string]int)}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(goZipPath, stub.handleZip)
|
|
mux.HandleFunc(goInfoPath, stub.handleInfo)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Skipf("unable to start go stub: %v", err)
|
|
}
|
|
server := &http.Server{Handler: mux}
|
|
stub.listener = listener
|
|
stub.server = server
|
|
stub.URL = "http://" + listener.Addr().String()
|
|
|
|
go func() {
|
|
_ = server.Serve(listener)
|
|
}()
|
|
return stub
|
|
}
|
|
|
|
func (s *goStub) handleZip(w http.ResponseWriter, r *http.Request) {
|
|
s.record(r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
_, _ = w.Write([]byte("zip-bytes"))
|
|
}
|
|
|
|
func (s *goStub) handleInfo(w http.ResponseWriter, r *http.Request) {
|
|
s.record(r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"Version":"v1.0.0"}`))
|
|
}
|
|
|
|
func (s *goStub) record(path string) {
|
|
s.mu.Lock()
|
|
s.hits[path]++
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *goStub) Close() {
|
|
if s == nil {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
if s.server != nil {
|
|
_ = s.server.Shutdown(ctx)
|
|
}
|
|
if s.listener != nil {
|
|
_ = s.listener.Close()
|
|
}
|
|
}
|