stage 1
This commit is contained in:
197
tests/integration/cache_strategy_override_test.go
Normal file
197
tests/integration/cache_strategy_override_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"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/hubmodule"
|
||||
"github.com/any-hub/any-hub/internal/proxy"
|
||||
"github.com/any-hub/any-hub/internal/server"
|
||||
)
|
||||
|
||||
func TestCacheStrategyOverrides(t *testing.T) {
|
||||
t.Run("ttl defers revalidation until expired", func(t *testing.T) {
|
||||
stub := newUpstreamStub(t, upstreamNPM)
|
||||
defer stub.Close()
|
||||
|
||||
storageDir := t.TempDir()
|
||||
ttl := 50 * time.Millisecond
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 6100,
|
||||
CacheTTL: config.Duration(time.Second),
|
||||
StoragePath: storageDir,
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "npm-ttl",
|
||||
Domain: "ttl.npm.local",
|
||||
Type: "npm",
|
||||
Module: "npm",
|
||||
Upstream: stub.URL,
|
||||
CacheTTL: config.Duration(ttl),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := newStrategyTestApp(t, cfg)
|
||||
|
||||
doRequest := func() *http.Response {
|
||||
req := httptest.NewRequest(http.MethodGet, "http://ttl.npm.local/lodash", nil)
|
||||
req.Host = "ttl.npm.local"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
resp := doRequest()
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if hit := resp.Header.Get("X-Any-Hub-Cache-Hit"); hit != "false" {
|
||||
t.Fatalf("first request should be miss, got %s", hit)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
resp2 := doRequest()
|
||||
if hit := resp2.Header.Get("X-Any-Hub-Cache-Hit"); hit != "true" {
|
||||
t.Fatalf("second request should hit cache before TTL, got %s", hit)
|
||||
}
|
||||
resp2.Body.Close()
|
||||
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 0 {
|
||||
t.Fatalf("expected no HEAD before TTL expiry, got %d", headCount)
|
||||
}
|
||||
if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 1 {
|
||||
t.Fatalf("upstream should be hit once before TTL expiry, got %d", getCount)
|
||||
}
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
|
||||
resp3 := doRequest()
|
||||
if hit := resp3.Header.Get("X-Any-Hub-Cache-Hit"); hit != "true" {
|
||||
body, _ := io.ReadAll(resp3.Body)
|
||||
resp3.Body.Close()
|
||||
t.Fatalf("expected cached response after HEAD revalidation, got %s body=%s", hit, string(body))
|
||||
}
|
||||
resp3.Body.Close()
|
||||
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 1 {
|
||||
t.Fatalf("expected single HEAD after TTL expiry, got %d", headCount)
|
||||
}
|
||||
if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 1 {
|
||||
t.Fatalf("upstream GET count should remain 1, got %d", getCount)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validation disabled falls back to refetch", func(t *testing.T) {
|
||||
stub := newUpstreamStub(t, upstreamNPM)
|
||||
defer stub.Close()
|
||||
|
||||
storageDir := t.TempDir()
|
||||
ttl := 25 * time.Millisecond
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 6200,
|
||||
CacheTTL: config.Duration(time.Second),
|
||||
StoragePath: storageDir,
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "npm-novalidation",
|
||||
Domain: "novalidation.npm.local",
|
||||
Type: "npm",
|
||||
Module: "npm",
|
||||
Upstream: stub.URL,
|
||||
CacheTTL: config.Duration(ttl),
|
||||
ValidationMode: string(hubmodule.ValidationModeNever),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := newStrategyTestApp(t, cfg)
|
||||
|
||||
doRequest := func() *http.Response {
|
||||
req := httptest.NewRequest(http.MethodGet, "http://novalidation.npm.local/lodash", nil)
|
||||
req.Host = "novalidation.npm.local"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test error: %v", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
first := doRequest()
|
||||
if first.Header.Get("X-Any-Hub-Cache-Hit") != "false" {
|
||||
t.Fatalf("expected miss on first request")
|
||||
}
|
||||
first.Body.Close()
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
|
||||
second := doRequest()
|
||||
if second.Header.Get("X-Any-Hub-Cache-Hit") != "false" {
|
||||
body, _ := io.ReadAll(second.Body)
|
||||
second.Body.Close()
|
||||
t.Fatalf("expected cache miss when validation disabled, got hit body=%s", string(body))
|
||||
}
|
||||
second.Body.Close()
|
||||
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 0 {
|
||||
t.Fatalf("validation mode never should avoid HEAD, got %d", headCount)
|
||||
}
|
||||
if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 2 {
|
||||
t.Fatalf("expected two upstream GETs due to forced refetch, got %d", getCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newStrategyTestApp(t *testing.T, cfg *config.Config) *fiber.App {
|
||||
t.Helper()
|
||||
|
||||
registry, err := server.NewHubRegistry(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("registry error: %v", err)
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
|
||||
store, err := cache.NewStore(cfg.Global.StoragePath)
|
||||
if err != nil {
|
||||
t.Fatalf("store error: %v", err)
|
||||
}
|
||||
|
||||
client := server.NewUpstreamClient(cfg)
|
||||
handler := proxy.NewHandler(client, 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 app
|
||||
}
|
||||
|
||||
func countRequests(reqs []RecordedRequest, method, path string) int {
|
||||
count := 0
|
||||
for _, req := range reqs {
|
||||
if req.Method == method && req.Path == path {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
118
tests/integration/legacy_adapter_toggle_test.go
Normal file
118
tests/integration/legacy_adapter_toggle_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule/legacy"
|
||||
"github.com/any-hub/any-hub/internal/server"
|
||||
)
|
||||
|
||||
func TestLegacyAdapterRolloutToggle(t *testing.T) {
|
||||
const moduleKey = "rollout-toggle-test"
|
||||
_ = hubmodule.Register(hubmodule.ModuleMetadata{Key: moduleKey})
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
|
||||
baseHub := config.HubConfig{
|
||||
Name: "dual-mode",
|
||||
Domain: "dual.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry.npmjs.org",
|
||||
Module: moduleKey,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rolloutFlag string
|
||||
expectKey string
|
||||
expectFlag legacy.RolloutFlag
|
||||
}{
|
||||
{
|
||||
name: "force legacy",
|
||||
rolloutFlag: "legacy-only",
|
||||
expectKey: hubmodule.DefaultModuleKey(),
|
||||
expectFlag: legacy.RolloutLegacyOnly,
|
||||
},
|
||||
{
|
||||
name: "dual mode",
|
||||
rolloutFlag: "dual",
|
||||
expectKey: moduleKey,
|
||||
expectFlag: legacy.RolloutDual,
|
||||
},
|
||||
{
|
||||
name: "full modular",
|
||||
rolloutFlag: "modular",
|
||||
expectKey: moduleKey,
|
||||
expectFlag: legacy.RolloutModular,
|
||||
},
|
||||
{
|
||||
name: "rollback to legacy",
|
||||
rolloutFlag: "legacy-only",
|
||||
expectKey: hubmodule.DefaultModuleKey(),
|
||||
expectFlag: legacy.RolloutLegacyOnly,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 6100,
|
||||
CacheTTL: config.Duration(time.Minute),
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
func() config.HubConfig {
|
||||
h := baseHub
|
||||
h.Rollout = tc.rolloutFlag
|
||||
return h
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
registry, err := server.NewHubRegistry(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build registry: %v", err)
|
||||
}
|
||||
|
||||
recorder := &routeRecorder{}
|
||||
app := mustNewApp(t, cfg.Global.ListenPort, logger, registry, recorder)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://dual.local/v2/", nil)
|
||||
req.Host = "dual.local"
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != fiber.StatusNoContent {
|
||||
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if recorder.moduleKey != tc.expectKey {
|
||||
t.Fatalf("expected module %s, got %s", tc.expectKey, recorder.moduleKey)
|
||||
}
|
||||
if recorder.rolloutFlag != tc.expectFlag {
|
||||
t.Fatalf("expected rollout flag %s, got %s", tc.expectFlag, recorder.rolloutFlag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type routeRecorder struct {
|
||||
moduleKey string
|
||||
rolloutFlag legacy.RolloutFlag
|
||||
}
|
||||
|
||||
func (r *routeRecorder) Handle(c fiber.Ctx, route *server.HubRoute) error {
|
||||
r.moduleKey = route.ModuleKey
|
||||
r.rolloutFlag = route.RolloutFlag
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
154
tests/integration/module_diagnostics_test.go
Normal file
154
tests/integration/module_diagnostics_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
"github.com/any-hub/any-hub/internal/hubmodule"
|
||||
"github.com/any-hub/any-hub/internal/server"
|
||||
"github.com/any-hub/any-hub/internal/server/routes"
|
||||
)
|
||||
|
||||
func TestModuleDiagnosticsEndpoints(t *testing.T) {
|
||||
const moduleKey = "diagnostics-test"
|
||||
_ = hubmodule.Register(hubmodule.ModuleMetadata{
|
||||
Key: moduleKey,
|
||||
Description: "diagnostics test module",
|
||||
MigrationState: hubmodule.MigrationStateBeta,
|
||||
SupportedProtocols: []string{
|
||||
"npm",
|
||||
},
|
||||
})
|
||||
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 6200,
|
||||
CacheTTL: config.Duration(30 * time.Minute),
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "legacy-hub",
|
||||
Domain: "legacy.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
},
|
||||
{
|
||||
Name: "modern-hub",
|
||||
Domain: "modern.local",
|
||||
Type: "npm",
|
||||
Upstream: "https://registry.npmjs.org",
|
||||
Module: moduleKey,
|
||||
Rollout: "dual",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registry, err := server.NewHubRegistry(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build registry: %v", err)
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
|
||||
app := mustNewApp(t, cfg.Global.ListenPort, logger, registry, server.ProxyHandlerFunc(func(c fiber.Ctx, _ *server.HubRoute) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}))
|
||||
routes.RegisterModuleRoutes(app, registry)
|
||||
|
||||
t.Run("list modules and hubs", func(t *testing.T) {
|
||||
resp := doRequest(t, app, "GET", "/-/modules")
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
var payload struct {
|
||||
Modules []map[string]any `json:"modules"`
|
||||
Hubs []struct {
|
||||
HubName string `json:"hub_name"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
Rollout string `json:"rollout_flag"`
|
||||
Domain string `json:"domain"`
|
||||
Port int `json:"port"`
|
||||
} `json:"hubs"`
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
t.Fatalf("failed to decode response: %v\nbody: %s", err, string(body))
|
||||
}
|
||||
if len(payload.Modules) == 0 {
|
||||
t.Fatalf("expected module metadata entries")
|
||||
}
|
||||
found := false
|
||||
for _, module := range payload.Modules {
|
||||
if module["key"] == moduleKey {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected module %s in diagnostics payload", moduleKey)
|
||||
}
|
||||
if len(payload.Hubs) != 2 {
|
||||
t.Fatalf("expected 2 hubs, got %d", len(payload.Hubs))
|
||||
}
|
||||
for _, hub := range payload.Hubs {
|
||||
switch hub.HubName {
|
||||
case "legacy-hub":
|
||||
if hub.ModuleKey != hubmodule.DefaultModuleKey() {
|
||||
t.Fatalf("legacy hub should expose legacy module, got %s", hub.ModuleKey)
|
||||
}
|
||||
case "modern-hub":
|
||||
if hub.ModuleKey != moduleKey {
|
||||
t.Fatalf("modern hub should expose %s, got %s", moduleKey, hub.ModuleKey)
|
||||
}
|
||||
if hub.Rollout != "dual" {
|
||||
t.Fatalf("modern hub rollout flag should be dual, got %s", hub.Rollout)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected hub %s", hub.HubName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inspect module by key", func(t *testing.T) {
|
||||
resp := doRequest(t, app, "GET", "/-/modules/"+moduleKey)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
var module map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err := json.Unmarshal(body, &module); err != nil {
|
||||
t.Fatalf("module inspect decode failed: %v", err)
|
||||
}
|
||||
if module["key"] != moduleKey {
|
||||
t.Fatalf("expected module key %s, got %v", moduleKey, module["key"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown module returns 404", func(t *testing.T) {
|
||||
resp := doRequest(t, app, "GET", "/-/modules/missing-module")
|
||||
if resp.StatusCode != fiber.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func doRequest(t *testing.T, app *fiber.App, method, url string) *http.Response {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, url, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request %s %s failed: %v", method, url, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -96,10 +96,12 @@ func mustNewApp(t *testing.T, port int, logger *logrus.Logger, registry *server.
|
||||
type moduleRecorder struct {
|
||||
routeName string
|
||||
moduleKey string
|
||||
rollout string
|
||||
}
|
||||
|
||||
func (p *moduleRecorder) Handle(c fiber.Ctx, route *server.HubRoute) error {
|
||||
p.routeName = route.Config.Name
|
||||
p.moduleKey = route.ModuleKey
|
||||
p.rollout = string(route.RolloutFlag)
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user