package app import ( "context" "errors" "io" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "testing" "time" "github.com/bnema/zerowrap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" adminhttp "github.com/bnema/gordon/internal/adapters/in/http/admin" pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki" inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks" out "github.com/bnema/gordon/internal/boundaries/out" "github.com/bnema/gordon/internal/domain" proxyusecase "github.com/bnema/gordon/internal/usecase/proxy" traffic "github.com/bnema/gordon/internal/usecase/traffic" ) func newNotFoundProxyService(t *testing.T) *proxyusecase.Service { t.Helper() configSvc := inmocks.NewMockConfigService(t) configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}).Maybe() return proxyusecase.NewService(configSvc, proxyusecase.Config{}) } // testAccessLogWriter is a thread-safe mock AccessLogWriter for tests. type testAccessLogWriter struct { mu sync.Mutex entries []out.AccessLogEntry } func (w *testAccessLogWriter) Write(entry out.AccessLogEntry) error { w.mu.Lock() defer w.mu.Unlock() w.entries = append(w.entries, entry) return nil } func (w *testAccessLogWriter) snapshot() []out.AccessLogEntry { w.mu.Lock() defer w.mu.Unlock() result := make([]out.AccessLogEntry, len(w.entries)) copy(result, w.entries) return result } func TestCreateAuthService_Disabled_ReturnsNilServices(t *testing.T) { t.Parallel() cfg := Config{} cfg.Auth.Enabled = false store, authSvc, err := createAuthService(context.Background(), cfg, zerowrap.Default()) if err != nil { t.Fatalf("expected no error when auth is disabled, got %v", err) } if store != nil { t.Fatalf("expected nil token store when auth is disabled") } if authSvc != nil { t.Fatalf("expected nil auth service when auth is disabled") } } func TestCreateHTTPHandlers_LocalMode_DisablesAdminRoutes(t *testing.T) { t.Parallel() cfg := Config{} cfg.Auth.Enabled = false svc := &services{adminHandler: &adminhttp.Handler{}} registryHandler, _, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) req := httptest.NewRequest(http.MethodGet, "/admin/status", nil) req.RemoteAddr = "192.0.2.10:12345" rec := httptest.NewRecorder() registryHandler.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("expected status %d, got %d", http.StatusNotFound, rec.Code) } } func TestCreateHTTPHandlers_LocalMode_RestrictsRegistryToLoopback(t *testing.T) { t.Parallel() cfg := Config{} cfg.Auth.Enabled = false registryHandler, _, _ := createHTTPHandlers(&services{}, cfg, zerowrap.Default(), nil) req := httptest.NewRequest(http.MethodGet, "/v2/test/manifests/latest", nil) req.RemoteAddr = "192.0.2.20:12345" rec := httptest.NewRecorder() registryHandler.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code) } } // newTestCA creates a temporary CA adapter for integration tests. func newTestCA(t *testing.T) *pkiadapter.CA { t.Helper() ca, err := pkiadapter.NewCA(t.TempDir(), zerowrap.Default()) require.NoError(t, err) return ca } // newOnboardingConfig returns a Config with trusted proxy CIDRs and TLS enabled. func newOnboardingConfig() Config { cfg := Config{} cfg.Server.Port = 8088 cfg.Server.TLSPort = 8443 cfg.Server.ProxyAllowedIPs = []string{"173.245.48.0/20"} // Cloudflare sample cfg.EntryPoints = map[string]traffic.EntryPointConfig{ traffic.DefaultEdgeEntryPointName: {Address: ":8443", Protocol: domain.EntryPointProtocolSmartTCP}, } return cfg } // directAddr is an IP outside the trusted proxy CIDR range. const directAddr = "203.0.113.10:12345" // trustedProxyAddr is an IP inside the 173.245.48.0/20 range. const trustedProxyAddr = "173.245.48.5:12345" func TestCreateHTTPHandlers_DirectHTTPOnboarding_AllPaths(t *testing.T) { t.Parallel() tests := []struct { name string path string wantCode int wantContent string // substring check wantType string // Content-Type substring }{ {name: ".well-known/gordon/ serves onboarding", path: "/.well-known/gordon/", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, {name: ".well-known/gordon/ca serves onboarding", path: "/.well-known/gordon/ca", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, {name: ".well-known/gordon/ca.crt serves CA cert", path: "/.well-known/gordon/ca.crt", wantCode: http.StatusOK, wantContent: "BEGIN CERTIFICATE", wantType: "application/x-x509-ca-cert"}, {name: ".well-known/gordon/ca.mobileconfig serves mobileconfig", path: "/.well-known/gordon/ca.mobileconfig", wantCode: http.StatusOK, wantContent: "