package main import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "git.cin.su/m/Assistant/contracts/cognitive" ) const legacyExternalMode = "legacy-external-test-mode" func newHandlerWithAdvisoryMode(coreURL, continuityURL, commitURL string, client *http.Client, mode string, cognitiveURLs ...string) (http.Handler, error) { if mode != legacyExternalMode { return nil, fmt.Errorf("invalid advisory binding mode %q", mode) } return newHandler(coreURL, continuityURL, commitURL, client, cognitiveURLs...) } func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/tasks/task-1" || r.URL.RawQuery != "session_id=session-1&workspace_id=workspace-1" { t.Fatalf("unexpected upstream target %s?%s", r.URL.Path, r.URL.RawQuery) } if r.Header.Get("Authorization") != "Bearer bounded-session" { t.Fatal("authorization header was not forwarded") } w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"TaskID":"task-1"}`) })) defer upstream.Close() handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/api/tasks/task-1?session_id=session-1&workspace_id=workspace-1", nil) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "task-1") { t.Fatalf("response = %d %s", response.Code, response.Body.String()) } } func TestGatewayForwardsCreatorCredentialsOnlyInBody(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/sessions" || r.Method != http.MethodPost { t.Fatalf("unexpected session target %s %s", r.Method, r.URL.Path) } if r.Header.Get("Authorization") != "" || r.Header.Get("Content-Type") != "application/json" { t.Fatalf("unexpected session headers Authorization=%q Content-Type=%q", r.Header.Get("Authorization"), r.Header.Get("Content-Type")) } body, _ := io.ReadAll(r.Body) if string(body) != `{"username":"m","password":"secret"}` { t.Fatalf("session body = %s", body) } _, _ = io.WriteString(w, `{"Token":"bounded"}`) })) defer upstream.Close() handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/session", strings.NewReader(`{"username":"m","password":"secret"}`)) request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("session response = %d %s", response.Code, response.Body.String()) } } func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) { handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient) if err != nil { t.Fatal(err) } for _, test := range []struct{ method, path string }{ {http.MethodPost, "/api/tasks/task-1"}, {http.MethodGet, "/api/principal-bindings/actor/principal"}, {http.MethodPost, "/api/focus-task-commits"}, {http.MethodPost, "/api/prospective-items/item-1"}, {http.MethodGet, "/api/tasks/../../healthz"}, {http.MethodPost, "/api/actions/arbitrary"}, {http.MethodGet, "/api/actions/satisfy-delivered"}, {http.MethodGet, "/api/actions/notify-delivered"}, } { request := httptest.NewRequest(test.method, test.path, nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusNotFound { t.Fatalf("%s %s returned %d", test.method, test.path, response.Code) } } } func TestGatewayRoutesProspectiveItemToContinuity(t *testing.T) { continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/prospective-items/item-1" { t.Fatalf("unexpected Continuity target %s", r.URL.Path) } if r.Header.Get("Authorization") != "Bearer bounded-session" { t.Fatal("authorization header was not forwarded") } _, _ = io.WriteString(w, `{"ID":"item-1"}`) })) defer continuity.Close() handler, err := newHandler("http://core-runtime.invalid", continuity.URL, "http://commit-runtime.invalid", continuity.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/api/prospective-items/item-1", nil) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "item-1") { t.Fatalf("response = %d %s", response.Code, response.Body.String()) } } func TestGatewayRoutesProspectiveListToContinuity(t *testing.T) { continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/prospective-items" || r.URL.RawQuery != "limit=25" { t.Fatalf("unexpected Continuity list target %s?%s", r.URL.Path, r.URL.RawQuery) } _, _ = io.WriteString(w, `{"Items":[]}`) })) defer continuity.Close() handler, err := newHandler("http://core-runtime.invalid", continuity.URL, "http://commit-runtime.invalid", continuity.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/api/prospective-items?limit=25", nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Items") { t.Fatalf("response = %d %s", response.Code, response.Body.String()) } } func TestGatewayRoutesOnlyBoundedCreatorCommandsToCommit(t *testing.T) { commitRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/continuity/satisfy-delivered" || r.Method != http.MethodPost { t.Fatalf("unexpected Commit target %s %s", r.Method, r.URL.Path) } if r.Header.Get("Authorization") != "Bearer bounded-session" { t.Fatal("authorization header was not forwarded") } body, _ := io.ReadAll(r.Body) if !strings.Contains(string(body), "prospective-1") || strings.Contains(string(body), "GrantID") || strings.Contains(string(body), "CapabilityID") { t.Fatalf("unexpected command body %s", body) } w.WriteHeader(http.StatusCreated) _, _ = io.WriteString(w, `{"State":"SUCCEEDED"}`) })) defer commitRuntime.Close() handler, err := newHandler("http://core-runtime.invalid", "http://continuity-runtime.invalid", commitRuntime.URL, commitRuntime.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/actions/satisfy-delivered", strings.NewReader(`{"ProspectiveItemID":"prospective-1"}`)) request.Header.Set("Authorization", "Bearer bounded-session") request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), "SUCCEEDED") { t.Fatalf("response = %d %s", response.Code, response.Body.String()) } } func TestGatewayRoutesTypedNotificationCommandToCommit(t *testing.T) { commitRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/continuity/notify-delivered" || r.Method != http.MethodPost { t.Fatalf("unexpected Commit target %s %s", r.Method, r.URL.Path) } body, _ := io.ReadAll(r.Body) text := string(body) if !strings.Contains(text, "prospective-1") || !strings.Contains(text, "Confirm") || strings.Contains(text, "URL") || strings.Contains(text, "CapabilityID") { t.Fatalf("unexpected notification body %s", text) } w.WriteHeader(http.StatusCreated) _, _ = io.WriteString(w, `{"State":"SUCCEEDED"}`) })) defer commitRuntime.Close() handler, err := newHandler("http://core-runtime.invalid", "http://continuity-runtime.invalid", commitRuntime.URL, commitRuntime.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/actions/notify-delivered", strings.NewReader(`{"ProspectiveItemID":"prospective-1","Confirm":true}`)) request.Header.Set("Authorization", "Bearer bounded-session") request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusCreated { t.Fatalf("response=%d %s", response.Code, response.Body.String()) } } func TestGatewayRoutesCreatorActionHistoryToCommit(t *testing.T) { commitRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/authorized-actions" || r.URL.RawQuery != "limit=25" || r.Method != http.MethodGet { t.Fatalf("unexpected Commit history target %s?%s %s", r.URL.Path, r.URL.RawQuery, r.Method) } if r.Header.Get("Authorization") != "Bearer bounded-session" { t.Fatal("authorization header was not forwarded") } _, _ = io.WriteString(w, `{"Items":[{"AuthorizedActionID":"authorized-1"}]}`) })) defer commitRuntime.Close() handler, err := newHandler("http://core-runtime.invalid", "http://continuity-runtime.invalid", commitRuntime.URL, commitRuntime.Client()) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/api/actions?limit=25", nil) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "authorized-1") { t.Fatalf("response = %d %s", response.Code, response.Body.String()) } } func TestConsoleAndSecurityHeaders(t *testing.T) { handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/", nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") || !strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") || !strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") || !strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") || !strings.Contains(response.Body.String(), "advisory-egress-ack") || !strings.Contains(response.Body.String(), "внешнему провайдеру") || !strings.Contains(response.Body.String(), "creator-username") || !strings.Contains(response.Body.String(), "creator-password") || strings.Contains(response.Body.String(), "bootstrap-token") { t.Fatalf("console response = %d", response.Code) } if response.Header().Get("Content-Security-Policy") == "" { t.Fatal("content security policy is missing") } } func TestAdvisoryModeRejectsInvalidConfiguration(t *testing.T) { _, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, "browser-selected") if err == nil || !strings.Contains(err.Error(), "invalid advisory binding mode") { t.Fatalf("error=%v", err) } } func TestLegacyExternalAdvisoryPolicyRequiresAuthenticatedCreatorSession(t *testing.T) { t.Skip("superseded by registry-derived advisory policy tests") core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" { t.Fatalf("unexpected policy authentication request %s %q", r.URL.Path, r.Header.Get("Authorization")) } _, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`) })) defer core.Close() handler, err := newHandlerWithAdvisoryMode(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), legacyExternalMode) if err != nil { t.Fatal(err) } denied := httptest.NewRecorder() handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/advisory-policy", nil)) if denied.Code != http.StatusUnauthorized { t.Fatalf("unauthenticated status=%d", denied.Code) } request := httptest.NewRequest(http.MethodGet, "/api/advisory-policy", nil) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || response.Header().Get("Cache-Control") != "no-store" { t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String()) } var policy advisoryPolicy if err := json.Unmarshal(response.Body.Bytes(), &policy); err != nil { t.Fatal(err) } if !policy.ExternalEgress || !policy.AcknowledgementRequired { t.Fatalf("unexpected policy %+v", policy) } } func TestLegacyExternalAdvisoryRequiresPerRequestAcknowledgementBeforeUpstreamCalls(t *testing.T) { t.Skip("acknowledgement is enforced by the cognitive gateway") called := false upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) defer upstream.Close() handler, err := newHandlerWithAdvisoryMode(upstream.URL, upstream.URL, upstream.URL, upstream.Client(), legacyExternalMode, upstream.URL) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","egress_acknowledged":false}`)) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusBadRequest || called { t.Fatalf("status=%d upstream_called=%t", response.Code, called) } } func TestLegacyExternalAdvisoryUsesOnlyServerSelectedRouteAndBinding(t *testing.T) { t.Skip("superseded by provider-neutral gateway route test") core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/creator/sessions/current": _, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`) case "/v1/creator/focus": _, _ = io.WriteString(w, `{"ID":"focus-1","ActorID":"creator-actor","SessionID":"focus-session","Workspace":"home","ScopeID":"trusted-scope","Active":true}`) default: t.Fatalf("unexpected core path %s", r.URL.Path) } })) defer core.Close() cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/advisories" { t.Fatalf("unexpected cognitive path %s", r.URL.Path) } var trusted cognitive.GatewayAdvisoryRequest if err := json.NewDecoder(r.Body).Decode(&trusted); err != nil { t.Fatal(err) } if len(trusted.Context) != 0 || trusted.Message != "help" { t.Fatalf("unexpected trusted request %+v", trusted) } _ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: trusted.RequestID, AdvisoryText: "Проверьте границы.", FinishReason: cognitive.FinishCompleted, Usage: &cognitive.ProviderUsage{InputUnits: 2, OutputUnits: 3}}) })) defer cognitiveRuntime.Close() handler, err := newHandlerWithAdvisoryMode(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), legacyExternalMode, cognitiveRuntime.URL) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","egress_acknowledged":true}`)) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Проверьте границы") { t.Fatalf("response=%d %s", response.Code, response.Body.String()) } } func TestLegacyExternalAdvisoryRejectsBrowserSelectedBinding(t *testing.T) { handler, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, legacyExternalMode) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","egress_acknowledged":true,"provider_binding_id":"attacker"}`)) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) } } func TestLegacyExternalAdvisoryResponseValidationFailsClosed(t *testing.T) { request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", MaxOutputBytes: 16} valid := cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "совет", FinishReason: cognitive.FinishCompleted} if !validAdvisoryResponse(valid, request) { t.Fatal("valid completed response was rejected") } invalid := []cognitive.GatewayAdvisoryResponse{ {Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted}, {Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "unexpected", FinishReason: cognitive.FinishTimeout}, {Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "0123456789abcdefg", FinishReason: cognitive.FinishCompleted}, {Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "ok", ReferencedExcerptIDs: []cognitive.ContextExcerptID{"untrusted"}, FinishReason: cognitive.FinishCompleted}, {Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted, Usage: &cognitive.ProviderUsage{InputUnits: -1}}, } for index, response := range invalid { if validAdvisoryResponse(response, request) { t.Fatalf("invalid response %d was accepted: %+v", index, response) } } } func TestAdvisoryRequiresSessionBeforeUpstreamCalls(t *testing.T) { called := false upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) defer upstream.Close() handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client(), upstream.URL) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home"}`)) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusUnauthorized || called { t.Fatalf("status=%d upstream_called=%t", response.Code, called) } } func TestAdvisoryAssemblesTrustedIdentityAndScope(t *testing.T) { longMessage := strings.Repeat("a", 16*1024) core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer bounded-session" { t.Fatal("missing bounded session") } w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/v1/creator/sessions/current": _, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`) case "/v1/creator/focus": if r.URL.Query().Get("session_id") != "focus-session" || r.URL.Query().Get("workspace_id") != "home" { t.Fatalf("unexpected focus query: %s", r.URL.RawQuery) } _, _ = io.WriteString(w, `{"ID":"focus-1","ActorID":"creator-actor","SessionID":"focus-session","Workspace":"home","ProjectID":"domovoy","GoalID":"goal-1","ScopeID":"trusted-scope","Active":true}`) default: t.Fatalf("unexpected core path %s", r.URL.Path) } })) defer core.Close() cognitive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) text := string(body) var trusted cognitive.AdvisoryRequest if err := json.Unmarshal(body, &trusted); err != nil { t.Fatal(err) } if trusted.ActorID != "creator-actor" || trusted.PrincipalID != "creator-principal" || trusted.ScopeID != "trusted-scope" || trusted.Message != longMessage || strings.Contains(text, "attacker") { t.Fatal("cognitive request was not assembled from trusted context") } var request map[string]any _ = json.Unmarshal(body, &request) _, _ = fmt.Fprintf(w, `{"Version":"creator-advisory-v1","RequestID":%q,"AdvisoryText":"","ReferencedExcerptIDs":null,"FinishReason":"PROVIDER_UNAVAILABLE","Usage":null}`, request["RequestID"]) })) defer cognitive.Close() handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitive.URL) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(fmt.Sprintf(`{"message":%q,"session_id":"focus-session","workspace_id":"home"}`, longMessage))) request.Header.Set("Authorization", "Bearer bounded-session") request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "PROVIDER_UNAVAILABLE") { t.Fatalf("response=%d %s", response.Code, response.Body.String()) } } func TestAdvisoryRejectsBrowserAuthorityFieldsBeforeCognitiveCall(t *testing.T) { called := false cognitive := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) defer cognitive.Close() handler, err := newHandler("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, cognitive.URL) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","ActorID":"attacker"}`)) request.Header.Set("Authorization", "Bearer bounded-session") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusBadRequest || called { t.Fatalf("status=%d cognitive_called=%t", response.Code, called) } }