package httpapi import ( "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "data-terminal-cloud/internal/alerts" "data-terminal-cloud/internal/billing" "data-terminal-cloud/internal/device" "data-terminal-cloud/internal/deviceauth" "data-terminal-cloud/internal/diagnostics" "data-terminal-cloud/internal/health" "data-terminal-cloud/internal/identity" "data-terminal-cloud/internal/media" "data-terminal-cloud/internal/opsaccess" "data-terminal-cloud/internal/opsconfig" "data-terminal-cloud/internal/saas" "data-terminal-cloud/internal/sharing" "data-terminal-cloud/internal/webevents" ) func TestLiveEndpointReturnsRequestID(t *testing.T) { checker := health.NewChecker(time.Second, map[string]health.CheckFunc{}) router := NewRouter(RouterOptions{ServiceName: "api", Readiness: checker}) handler := Middleware(slog.Default(), "test", 1024, nil, router) request := httptest.NewRequest(http.MethodGet, "/health/live", nil) request.Header.Set("X-Request-Id", "test-request") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK) } if recorder.Header().Get("X-Request-Id") != "test-request" { t.Fatalf("request id header was not preserved") } var body map[string]any if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatalf("decode response: %v", err) } if body["request_id"] != "test-request" { t.Fatalf("unexpected request_id: %#v", body["request_id"]) } } func TestMetricsExposeHTTPSeries(t *testing.T) { handler := Middleware(slog.Default(), "metrics-test", 1024, nil, http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusNoContent) })) request := httptest.NewRequest(http.MethodGet, "/example", nil) request.Pattern = "GET /example" handler.ServeHTTP(httptest.NewRecorder(), request) recorder := httptest.NewRecorder() MetricsHandler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/metrics", nil)) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `voice_platform_http_requests_total{method="GET",route="GET /example",service="metrics-test",status="204"} 1`) { t.Fatalf("HTTP metric missing from scrape") } } func TestReadyEndpointReturnsServiceUnavailable(t *testing.T) { checker := health.NewChecker(time.Second, map[string]health.CheckFunc{ "database": func(context.Context) error { return context.DeadlineExceeded }, }) router := NewRouter(RouterOptions{ServiceName: "api", Readiness: checker}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/health/ready", nil)) if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusServiceUnavailable) } } func TestNewRouterRegistersCompleteAPIAndCallbackSurface(t *testing.T) { checker := health.NewChecker(time.Second, map[string]health.CheckFunc{}) apiRouter := NewRouter(RouterOptions{ ServiceName: "api", Readiness: checker, TokenVerifier: new(identity.OIDCVerifier), IdentityRepository: new(identity.Repository), SaaSService: new(saas.Service), DeviceVerifier: fakeDeviceVerifier{err: deviceauth.ErrCredentialsMissing}, DeviceService: new(device.Repository), DeviceTelemetrySubmitter: &fakeTelemetrySubmitter{}, MediaService: new(media.Service), WebEventService: new(webevents.Service), SharingService: new(sharing.Service), OpsConfigService: new(opsconfig.Service), BillingService: new(billing.Service), AlertService: new(alerts.Service), DiagnosticService: new(diagnostics.Service), OpsAccessService: new(opsaccess.Service), ConversationCreator: &fakeConversationCreator{}, ConversationActivator: &fakeConversationActivator{}, DeviceConversationStateReader: &fakeDeviceConversationStateReader{}, ConversationHeartbeater: &fakeConversationHeartbeater{}, ConversationStopper: &fakeConversationStopper{}, ConversationTranscriptSubmitter: &fakeConversationTranscriptSubmitter{}, }) for _, target := range []string{ "/api/v1/me", "/api/v1/households", "/api/v1/ops/alerts", "/api/v1/ops/sensitive-access-requests", "/api/v1/events", } { recorder := httptest.NewRecorder() apiRouter.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, target, nil)) if recorder.Code != http.StatusUnauthorized { t.Fatalf("protected route %s status=%d body=%s", target, recorder.Code, recorder.Body.String()) } } recorder := httptest.NewRecorder() apiRouter.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/device/v2/bootstrap", nil)) if recorder.Code != http.StatusUnauthorized { t.Fatalf("device route status=%d body=%s", recorder.Code, recorder.Body.String()) } _, pattern := apiRouter.(*http.ServeMux).Handler(httptest.NewRequest(http.MethodGet, "/api/v1/public/shares/"+strings.Repeat("a", 36), nil)) if pattern != "GET /api/v1/public/shares/{id}" { t.Fatalf("public sharing route pattern=%q", pattern) } callbackRouter := NewRouter(RouterOptions{ ServiceName: "callbacks", Readiness: checker, ProviderCallbackSecret: []byte("01234567890123456789012345678901"), ProviderCallbackSkew: time.Minute, ProviderCallbackProcessor: &fakeProviderCallbackProcessor{}, }) recorder = httptest.NewRecorder() callbackRouter.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/callbacks/v1/vertc/voice-chat", strings.NewReader(`{}`))) if recorder.Code != http.StatusUnauthorized { t.Fatalf("callback route status=%d body=%s", recorder.Code, recorder.Body.String()) } }