package server_test import ( "context" "fmt" "io" "net/http" "testing" "time" "github.com/Riotpiaole/homelab-frontend/internal/server" ) // TestHealthEndpoints verifies health endpoint behavior. // - /healthz returns 200 even with unreachable upstreams // - /readyz returns non-2xx before JWKS fetch and 200 after // - Neither endpoint requires authentication func TestHealthEndpoints(t *testing.T) { tests := []struct { name string configValid bool authEnabled bool jwksFetched bool endpoint string expectedCode int description string }{ { name: "healthz_always_200", configValid: true, authEnabled: false, jwksFetched: false, endpoint: "/healthz", expectedCode: http.StatusOK, description: "liveness probe returns 200 even before JWKS fetch", }, { name: "healthz_200_when_config_invalid", configValid: false, authEnabled: false, jwksFetched: false, endpoint: "/healthz", expectedCode: http.StatusOK, description: "liveness probe returns 200 even when config is invalid", }, { name: "readyz_200_no_auth", configValid: true, authEnabled: false, jwksFetched: false, endpoint: "/readyz", expectedCode: http.StatusOK, description: "readiness returns 200 when config valid and auth disabled", }, { name: "readyz_503_invalid_config", configValid: false, authEnabled: false, jwksFetched: false, endpoint: "/readyz", expectedCode: http.StatusServiceUnavailable, description: "readiness returns 503 when config invalid", }, { name: "readyz_503_auth_enabled_no_jwks", configValid: true, authEnabled: true, jwksFetched: false, endpoint: "/readyz", expectedCode: http.StatusServiceUnavailable, description: "readiness returns 503 when auth enabled but JWKS not fetched", }, { name: "readyz_200_auth_enabled_with_jwks", configValid: true, authEnabled: true, jwksFetched: true, endpoint: "/readyz", expectedCode: http.StatusOK, description: "readiness returns 200 when auth enabled and JWKS fetched", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create health checker hc := server.NewHealthChecker(tt.configValid, tt.authEnabled) if tt.jwksFetched { hc.MarkJWKSFetched() } // Create handler based on endpoint var handler http.HandlerFunc switch tt.endpoint { case "/healthz": handler = server.LivenessHandler(hc) case "/readyz": handler = server.ReadinessHandler(hc) default: t.Fatalf("unknown endpoint: %s", tt.endpoint) } // Create server wrapper gatewayServer := server.New("127.0.0.1:0", 5*time.Second, handler) // Start server in goroutine go func() { if err := gatewayServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { t.Logf("server error: %v", err) } }() // Give server time to start time.Sleep(100 * time.Millisecond) // Make request url := fmt.Sprintf("http://%s%s", gatewayServer.Addr(), tt.endpoint) resp, err := http.Get(url) if err != nil { t.Fatalf("failed to make request: %v", err) } defer resp.Body.Close() // Check status code if resp.StatusCode != tt.expectedCode { body, _ := io.ReadAll(resp.Body) t.Errorf("expected status %d, got %d: %s", tt.expectedCode, resp.StatusCode, string(body)) } // Verify no Authorization header is required // (we already made the request without one, so this is implicit) // Cleanup gatewayServer.Shutdown(context.Background()) }) } } // TestHealthEndpointsNoProxy verifies that health endpoints are not proxied. // This is verified indirectly by the test above - if they were proxied, // they would return 404 or fail when trying to reach a non-existent upstream. func TestHealthEndpointsCannotBeShadowed(t *testing.T) { // Create health checker and handler hc := server.NewHealthChecker(true, false) handler := server.LivenessHandler(hc) // Create server srv := server.New("127.0.0.1:0", 5*time.Second, handler) // Start server go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { t.Logf("server error: %v", err) } }() // Give server time to start time.Sleep(100 * time.Millisecond) // Request /healthz and verify it's not proxied resp, err := http.Get(fmt.Sprintf("http://%s/healthz", srv.Addr())) if err != nil { t.Fatalf("failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected status 200, got %d", resp.StatusCode) } // Cleanup srv.Shutdown(context.Background()) }