This commit is contained in:
2025-11-17 15:39:44 +08:00
parent abfa51f12e
commit 1ddda89499
46 changed files with 2185 additions and 751 deletions

View File

@@ -0,0 +1,297 @@
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()
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/any-hub/any-hub/internal/config"
"github.com/any-hub/any-hub/internal/hubmodule"
"github.com/any-hub/any-hub/internal/proxy/hooks"
"github.com/any-hub/any-hub/internal/server"
"github.com/any-hub/any-hub/internal/server/routes"
)
@@ -27,6 +28,7 @@ func TestModuleDiagnosticsEndpoints(t *testing.T) {
"npm",
},
})
hooks.MustRegister(moduleKey, hooks.Hooks{})
cfg := &config.Config{
Global: config.GlobalConfig{
@@ -77,6 +79,7 @@ func TestModuleDiagnosticsEndpoints(t *testing.T) {
Rollout string `json:"rollout_flag"`
Domain string `json:"domain"`
Port int `json:"port"`
LegacyOnly bool `json:"legacy_only"`
} `json:"hubs"`
}
body, _ := io.ReadAll(resp.Body)
@@ -90,6 +93,9 @@ func TestModuleDiagnosticsEndpoints(t *testing.T) {
found := false
for _, module := range payload.Modules {
if module["key"] == moduleKey {
if module["hook_status"] != "registered" {
t.Fatalf("expected module %s hook_status registered, got %v", moduleKey, module["hook_status"])
}
found = true
break
}
@@ -106,6 +112,9 @@ func TestModuleDiagnosticsEndpoints(t *testing.T) {
if hub.ModuleKey != hubmodule.DefaultModuleKey() {
t.Fatalf("legacy hub should expose legacy module, got %s", hub.ModuleKey)
}
if !hub.LegacyOnly {
t.Fatalf("legacy hub should be marked legacy_only")
}
case "modern-hub":
if hub.ModuleKey != moduleKey {
t.Fatalf("modern hub should expose %s, got %s", moduleKey, hub.ModuleKey)
@@ -113,6 +122,9 @@ func TestModuleDiagnosticsEndpoints(t *testing.T) {
if hub.Rollout != "dual" {
t.Fatalf("modern hub rollout flag should be dual, got %s", hub.Rollout)
}
if hub.LegacyOnly {
t.Fatalf("modern hub should not be marked legacy_only")
}
default:
t.Fatalf("unexpected hub %s", hub.HubName)
}