Files
Assistant/cmd/creator-web/server_test.go
m 59f5b4ce57
CI / test (push) Successful in 11s
CI / test (pull_request) Successful in 11s
fix: honor advisory message size contract
2026-08-16 20:22:27 +03:00

320 lines
15 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/cognitive"
)
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(), "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 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,"ProviderBindingID":"deterministic-fake","ProviderBindingRevision":1,"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)
}
}