package core import ( "bytes" "context" "encoding/base64" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/gorilla/websocket" ) // helpers ------------------------------------------------------------------ func startTestBridge(t *testing.T, token string) (*BridgeServer, string) { t.Helper() var bs *BridgeServer if token == "" { // Use insecure mode for tests without token bs = NewBridgeServerInsecure(0, token, "/bridge/ws", nil) } else { bs = NewBridgeServer(0, token, "/bridge/ws", nil) } if bs == nil { t.Fatalf("failed to create bridge server") } mux := http.NewServeMux() mux.HandleFunc("/bridge/ws", bs.handleWS) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/bridge/ws" return bs, wsURL } func dialWS(t *testing.T, url string, headers http.Header) *websocket.Conn { t.Helper() conn, _, err := websocket.DefaultDialer.Dial(url, headers) if err != nil { t.Fatalf("dial websocket: %v", err) } t.Cleanup(func() { conn.Close() }) return conn } func register(t *testing.T, conn *websocket.Conn, platform string, caps []string) { t.Helper() msg := map[string]any{ "type": "register", "platform": platform, "capabilities": caps, } mustWriteJSON(t, conn, msg) var ack map[string]any mustReadJSON(t, conn, &ack) if ack["ok"] != true { t.Fatalf("register failed: %v", ack["error"]) } } func registerWithMetadata(t *testing.T, conn *websocket.Conn, platform string, caps []string, metadata map[string]any) { t.Helper() msg := map[string]any{ "type": "register", "platform": platform, "capabilities": caps, "metadata": metadata, } mustWriteJSON(t, conn, msg) var ack map[string]any mustReadJSON(t, conn, &ack) if ack["ok"] != true { t.Fatalf("register failed: %v", ack["error"]) } } func readMsg(t *testing.T, conn *websocket.Conn) map[string]any { t.Helper() if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { t.Fatalf("set read deadline: %v", err) } var m map[string]any if err := conn.ReadJSON(&m); err != nil { t.Fatalf("read message: %v", err) } return m } func mustWriteJSON(t *testing.T, conn *websocket.Conn, v any) { t.Helper() if err := conn.WriteJSON(v); err != nil { t.Fatalf("write JSON: %v", err) } } func mustReadJSON(t *testing.T, conn *websocket.Conn, v any) { t.Helper() if err := conn.ReadJSON(v); err != nil { t.Fatalf("read JSON: %v", err) } } func mustDecodeJSON(t *testing.T, r io.Reader, v any) { t.Helper() if err := json.NewDecoder(r).Decode(v); err != nil { t.Fatalf("decode JSON: %v", err) } } func mustEncodeJSON(t *testing.T, w io.Writer, v any) { t.Helper() if err := json.NewEncoder(w).Encode(v); err != nil { t.Fatalf("encode JSON: %v", err) } } func mustUnmarshalJSON(t *testing.T, data []byte, v any) { t.Helper() if err := json.Unmarshal(data, v); err != nil { t.Fatalf("unmarshal JSON: %v", err) } } // tests -------------------------------------------------------------------- func TestBridge_RegisterAndConnect(t *testing.T) { bs, wsURL := startTestBridge(t, "") conn := dialWS(t, wsURL, nil) register(t, conn, "test-chat", []string{"text", "buttons"}) adapters := bs.ConnectedAdapters() if len(adapters) != 1 || adapters[0] != "test-chat" { t.Fatalf("expected [test-chat], got %v", adapters) } } func TestBridge_RegisterSendsCapabilitiesSnapshotWhenAdapterSupportsIt(t *testing.T) { prevVersion, prevCommit, prevBuildTime := CurrentVersion, CurrentCommit, CurrentBuildTime CurrentVersion = "v2.0.0" CurrentCommit = "deadbeef" CurrentBuildTime = "2026-04-11T00:00:00Z" defer func() { CurrentVersion = prevVersion CurrentCommit = prevCommit CurrentBuildTime = prevBuildTime }() bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("test-proj") e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish) e.AddCommand("deploy", "Deploy app", "ship it", "", "", "config") bs.RegisterEngine("test-proj", e, bp) conn := dialWS(t, wsURL, nil) registerWithMetadata(t, conn, "bridge", []string{"text"}, map[string]any{ "control_plane": []string{bridgeCapabilitiesSnapshotProto}, }) msg := readMsg(t, conn) if msg["type"] != bridgeCapabilitiesSnapshotType { t.Fatalf("type = %v, want %q", msg["type"], bridgeCapabilitiesSnapshotType) } if got := int(msg["v"].(float64)); got != 1 { t.Fatalf("v = %d, want 1", got) } host, ok := msg["host"].(map[string]any) if !ok { t.Fatalf("host = %T, want object", msg["host"]) } if host["cc_connect_version"] != "v2.0.0" { t.Fatalf("cc_connect_version = %v, want %q", host["cc_connect_version"], "v2.0.0") } projects, ok := msg["projects"].([]any) if !ok || len(projects) != 1 { t.Fatalf("projects = %T/%d, want 1 project", msg["projects"], len(projects)) } project, ok := projects[0].(map[string]any) if !ok { t.Fatalf("project = %T, want object", projects[0]) } if project["project"] != "test-proj" { t.Fatalf("project name = %v, want %q", project["project"], "test-proj") } commands, ok := project["commands"].([]any) if !ok || len(commands) == 0 { t.Fatalf("commands = %T/%d, want non-empty list", project["commands"], len(commands)) } foundDeploy := false for _, raw := range commands { cmd, ok := raw.(map[string]any) if !ok { t.Fatalf("command = %T, want object", raw) } if cmd["name"] == "deploy" { foundDeploy = true } } if !foundDeploy { t.Fatal("expected deploy command in capabilities snapshot") } } func TestBridge_AuthRequired(t *testing.T) { _, wsURL := startTestBridge(t, "secret123") // No auth → should fail _, resp, err := websocket.DefaultDialer.Dial(wsURL, nil) if err == nil { t.Fatal("expected connection to be rejected") } if resp != nil && resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", resp.StatusCode) } // With auth → should succeed headers := http.Header{} headers.Set("Authorization", "Bearer secret123") conn := dialWS(t, wsURL, headers) register(t, conn, "authed-chat", []string{"text"}) } func TestBridge_AuthQueryParam(t *testing.T) { _, wsURL := startTestBridge(t, "qtoken") conn := dialWS(t, wsURL+"?token=qtoken", nil) register(t, conn, "qp-chat", []string{"text"}) } func TestBridge_RegisterMissingPlatform(t *testing.T) { _, wsURL := startTestBridge(t, "") conn := dialWS(t, wsURL, nil) mustWriteJSON(t, conn, map[string]any{ "type": "register", "platform": "", "capabilities": []string{"text"}, }) var ack map[string]any mustReadJSON(t, conn, &ack) if ack["ok"] == true { t.Fatal("expected registration to fail for empty platform") } } func TestBridge_MessageRouting(t *testing.T) { bs, wsURL := startTestBridge(t, "") var received *Message var receivedMu sync.Mutex bp := bs.NewPlatform("test-proj") e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish) bs.RegisterEngine("test-proj", e, bp) bp.handler = func(p Platform, msg *Message) { receivedMu.Lock() received = msg receivedMu.Unlock() } conn := dialWS(t, wsURL, nil) register(t, conn, "mychat", []string{"text"}) imgData := base64.StdEncoding.EncodeToString([]byte("fakepng")) mustWriteJSON(t, conn, map[string]any{ "type": "message", "msg_id": "m1", "session_key": "mychat:user1:user1", "user_id": "user1", "user_name": "Alice", "content": "hello bridge", "reply_ctx": "conv-1", "images": []map[string]any{{"mime_type": "image/png", "data": imgData, "file_name": "test.png"}}, }) time.Sleep(100 * time.Millisecond) receivedMu.Lock() defer receivedMu.Unlock() if received == nil { t.Fatal("expected message to be received") } if received.Content != "hello bridge" { t.Fatalf("content = %q, want %q", received.Content, "hello bridge") } if received.Platform != "mychat" { t.Fatalf("platform = %q, want %q", received.Platform, "mychat") } if received.UserName != "Alice" { t.Fatalf("user_name = %q, want %q", received.UserName, "Alice") } if len(received.Images) != 1 { t.Fatalf("images count = %d, want 1", len(received.Images)) } if received.Images[0].FileName != "test.png" { t.Fatalf("image filename = %q, want %q", received.Images[0].FileName, "test.png") } } func TestBridge_MessageReplyCtxCarriesProgressHints(t *testing.T) { bs, wsURL := startTestBridge(t, "") gotCh := make(chan *bridgeReplyCtx, 1) bp := bs.NewPlatform("test-proj") e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish) bs.RegisterEngine("test-proj", e, bp) bp.handler = func(p Platform, msg *Message) { rc, ok := msg.ReplyCtx.(*bridgeReplyCtx) if !ok { t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", msg.ReplyCtx) } gotCh <- rc } conn := dialWS(t, wsURL, nil) registerWithMetadata(t, conn, "bridge", []string{"text", "card", "preview", "update_message"}, map[string]any{ "adapter": "bot-gateway", }) mustWriteJSON(t, conn, map[string]any{ "type": "message", "msg_id": "m1", "session_key": "bridge:room-1:user-1", "user_id": "user-1", "content": "hello", "reply_ctx": "ctx-1", }) var got *bridgeReplyCtx select { case got = <-gotCh: case <-time.After(5 * time.Second): t.Fatal("expected reply ctx to be captured") } if got.progressStyleHint() != progressStyleCard { t.Fatalf("progressStyleHint() = %q, want %q", got.progressStyleHint(), progressStyleCard) } if !got.supportsProgressCardPayloadHint() { t.Fatal("supportsProgressCardPayloadHint() = false, want true") } } func TestBridge_ReplyRouting(t *testing.T) { bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("test-proj") e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish) bs.RegisterEngine("test-proj", e, bp) bp.handler = func(p Platform, msg *Message) { if err := p.Reply(context.TODO(), msg.ReplyCtx, "pong"); err != nil { t.Fatalf("Reply: %v", err) } } conn := dialWS(t, wsURL, nil) register(t, conn, "rc", []string{"text"}) mustWriteJSON(t, conn, map[string]any{ "type": "message", "msg_id": "m1", "session_key": "rc:u1:u1", "user_id": "u1", "content": "ping", "reply_ctx": "ctx-1", }) reply := readMsg(t, conn) if reply["type"] != "reply" { t.Fatalf("type = %q, want reply", reply["type"]) } if reply["content"] != "pong" { t.Fatalf("content = %q, want pong", reply["content"]) } if reply["reply_ctx"] != "ctx-1" { t.Fatalf("reply_ctx = %q, want ctx-1", reply["reply_ctx"]) } } func TestBridge_ReconstructReplyCtx_RequiresCapability(t *testing.T) { bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("advisor-gemini") conn := dialWS(t, wsURL, nil) register(t, conn, "bridge", []string{"text"}) _, err := bp.ReconstructReplyCtx("bridge:1491487450722341088:relay") if err == nil || !strings.Contains(err.Error(), "does not support reconstruct_reply") { t.Fatalf("ReconstructReplyCtx() error = %v, want reconstruct_reply capability error", err) } } func TestBridge_ReconstructReplyCtx_UsesStructuredPayload(t *testing.T) { bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("advisor-gemini") conn := dialWS(t, wsURL, nil) register(t, conn, "bridge", []string{"text", "reconstruct_reply"}) replyCtx, err := bp.ReconstructReplyCtx("bridge:1491487450722341088:relay") if err != nil { t.Fatalf("ReconstructReplyCtx() error = %v", err) } rc, ok := replyCtx.(*bridgeReplyCtx) if !ok { t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", replyCtx) } if rc.Platform != "bridge" { t.Fatalf("Platform = %q, want bridge", rc.Platform) } if rc.SessionKey != "bridge:1491487450722341088:relay" { t.Fatalf("SessionKey = %q, want relay session key", rc.SessionKey) } var payload bridgeReconstructReplyCtxPayload if err := json.Unmarshal([]byte(rc.ReplyCtx), &payload); err != nil { t.Fatalf("unmarshal reply_ctx: %v", err) } if payload.Kind != bridgeReconstructReplyCtxKind { t.Fatalf("kind = %q, want %q", payload.Kind, bridgeReconstructReplyCtxKind) } if payload.Version != 1 { t.Fatalf("version = %d, want 1", payload.Version) } if payload.SenderProject != "advisor-gemini" { t.Fatalf("sender_project = %q, want advisor-gemini", payload.SenderProject) } if payload.TransportChatID != "1491487450722341088" { t.Fatalf("transport_chat_id = %q, want 1491487450722341088", payload.TransportChatID) } if payload.TransportSessionKey != "bridge:1491487450722341088:relay" { t.Fatalf("transport_session_key = %q, want relay session key", payload.TransportSessionKey) } } func TestBridge_ReconstructReplyCtx_UsesAdapterProgressHints(t *testing.T) { bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("test-proj") conn := dialWS(t, wsURL, nil) registerWithMetadata(t, conn, "bridge", []string{"text", "card", "preview", "update_message", "reconstruct_reply"}, map[string]any{ "adapter": "bot-gateway", }) replyCtx, err := bp.ReconstructReplyCtx("bridge:room-1:user-1") if err != nil { t.Fatalf("ReconstructReplyCtx() error = %v", err) } rc, ok := replyCtx.(*bridgeReplyCtx) if !ok { t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", replyCtx) } if rc.progressStyleHint() != progressStyleCard { t.Fatalf("progressStyleHint() = %q, want %q", rc.progressStyleHint(), progressStyleCard) } if !rc.supportsProgressCardPayloadHint() { t.Fatal("supportsProgressCardPayloadHint() = false, want true") } } func TestBridge_CardFallback(t *testing.T) { bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("test-proj") e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish) bs.RegisterEngine("test-proj", e, bp) bp.handler = func(p Platform, msg *Message) { cs, ok := p.(CardSender) if !ok { t.Fatal("BridgePlatform should implement CardSender") } card := NewCard().Title("Test", "blue").Markdown("hello").Build() if err := cs.SendCard(context.TODO(), msg.ReplyCtx, card); err != nil { t.Fatalf("SendCard: %v", err) } } // Adapter declares NO card capability → should get text fallback conn := dialWS(t, wsURL, nil) register(t, conn, "nocards", []string{"text"}) mustWriteJSON(t, conn, map[string]any{ "type": "message", "msg_id": "m1", "session_key": "nocards:u1:u1", "user_id": "u1", "content": "hi", "reply_ctx": "c1", }) reply := readMsg(t, conn) if reply["type"] != "reply" { t.Fatalf("expected text fallback, got type=%q", reply["type"]) } content, _ := reply["content"].(string) if !strings.Contains(content, "hello") { t.Fatalf("fallback should contain 'hello', got %q", content) } } func TestBridge_CardNative(t *testing.T) { bs, wsURL := startTestBridge(t, "") bp := bs.NewPlatform("test-proj") e := NewEngine("test-proj", &stubAgent{}, []Platform{bp}, "", LangEnglish) bs.RegisterEngine("test-proj", e, bp) bp.handler = func(p Platform, msg *Message) { cs := p.(CardSender) card := NewCard().Title("Test", "blue").Markdown("hello").Build() if err := cs.SendCard(context.TODO(), msg.ReplyCtx, card); err != nil { t.Fatalf("SendCard: %v", err) } } // Adapter declares card capability → should get card conn := dialWS(t, wsURL, nil) register(t, conn, "withcards", []string{"text", "card"}) mustWriteJSON(t, conn, map[string]any{ "type": "message", "msg_id": "m1", "session_key": "withcards:u1:u1", "user_id": "u1", "content": "hi", "reply_ctx": "c1", }) reply := readMsg(t, conn) if reply["type"] != "card" { t.Fatalf("expected card, got type=%q", reply["type"]) } cardData, ok := reply["card"].(map[string]any) if !ok { t.Fatal("card field should be a map") } header, _ := cardData["header"].(map[string]any) if header["title"] != "Test" { t.Fatalf("card title = %q, want Test", header["title"]) } } func TestBridge_Ping(t *testing.T) { _, wsURL := startTestBridge(t, "") conn := dialWS(t, wsURL, nil) register(t, conn, "pingtest", []string{"text"}) mustWriteJSON(t, conn, map[string]any{"type": "ping", "ts": time.Now().UnixMilli()}) pong := readMsg(t, conn) if pong["type"] != "pong" { t.Fatalf("expected pong, got %q", pong["type"]) } } func TestBridge_AdapterReplace(t *testing.T) { bs, wsURL := startTestBridge(t, "") conn1 := dialWS(t, wsURL, nil) register(t, conn1, "replaceme", []string{"text"}) if len(bs.ConnectedAdapters()) != 1 { t.Fatal("expected 1 adapter") } conn2 := dialWS(t, wsURL, nil) register(t, conn2, "replaceme", []string{"text", "card"}) if len(bs.ConnectedAdapters()) != 1 { t.Fatal("expected still 1 adapter after replace") } a := bs.getAdapter("replaceme") if !a.capabilities["card"] { t.Fatal("replaced adapter should have card capability") } } func TestSerializeCard(t *testing.T) { card := NewCard(). Title("Model", "blue"). Markdown("Choose:"). Buttons(PrimaryBtn("GPT-4", "cmd:/model switch gpt-4"), DefaultBtn("Claude", "cmd:/model switch claude")). Divider(). Note("tip"). Build() result := serializeCard(card) header, _ := result["header"].(map[string]string) if header["title"] != "Model" || header["color"] != "blue" { t.Fatalf("header = %v", header) } elements, _ := result["elements"].([]map[string]any) if len(elements) != 4 { t.Fatalf("elements count = %d, want 4", len(elements)) } if elements[0]["type"] != "markdown" { t.Fatalf("first element type = %q", elements[0]["type"]) } if elements[1]["type"] != "actions" { t.Fatalf("second element type = %q", elements[1]["type"]) } if elements[2]["type"] != "divider" { t.Fatalf("third element type = %q", elements[2]["type"]) } if elements[3]["type"] != "note" { t.Fatalf("fourth element type = %q", elements[3]["type"]) } btns, _ := elements[1]["buttons"].([]map[string]any) if len(btns) != 2 { t.Fatalf("buttons count = %d", len(btns)) } if btns[0]["text"] != "GPT-4" || btns[0]["value"] != "cmd:/model switch gpt-4" { t.Fatalf("button[0] = %v", btns[0]) } data, err := json.Marshal(result) if err != nil { t.Fatalf("json marshal: %v", err) } if len(data) == 0 { t.Fatal("serialized card is empty") } } // --------------------------------------------------------------------------- // Session Management REST API tests // --------------------------------------------------------------------------- // startTestBridgeWithREST creates a bridge server with both WS and REST endpoints. func startTestBridgeWithREST(t *testing.T, token string) (*BridgeServer, string) { t.Helper() var bs *BridgeServer if token == "" { bs = NewBridgeServerInsecure(0, token, "/bridge/ws", nil) } else { bs = NewBridgeServer(0, token, "/bridge/ws", nil) } if bs == nil { t.Fatalf("failed to create bridge server") } agent := &stubAgent{} sm := NewSessionManager("") engine := NewEngine("test-proj", agent, nil, "", LangEnglish) engine.sessions = sm bp := bs.NewPlatform("test-proj") bs.RegisterEngine("test-proj", engine, bp) mux := http.NewServeMux() mux.HandleFunc("/bridge/ws", bs.handleWS) mux.HandleFunc("/bridge/sessions", bs.authHTTP(bs.handleSessions)) mux.HandleFunc("/bridge/sessions/", bs.authHTTP(bs.handleSessionRoutes)) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return bs, srv.URL } type bridgeAPIResponse struct { OK bool `json:"ok"` Data json.RawMessage `json:"data,omitempty"` Error string `json:"error,omitempty"` } func bridgeGet(t *testing.T, url, token string) bridgeAPIResponse { t.Helper() req, _ := http.NewRequest("GET", url, nil) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET %s: %v", url, err) } defer resp.Body.Close() var r bridgeAPIResponse mustDecodeJSON(t, resp.Body, &r) return r } func bridgePost(t *testing.T, url, token string, body any) bridgeAPIResponse { t.Helper() var buf bytes.Buffer if body != nil { mustEncodeJSON(t, &buf, body) } req, _ := http.NewRequest("POST", url, &buf) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("POST %s: %v", url, err) } defer resp.Body.Close() var r bridgeAPIResponse mustDecodeJSON(t, resp.Body, &r) return r } func bridgeDel(t *testing.T, url, token string) bridgeAPIResponse { t.Helper() req, _ := http.NewRequest("DELETE", url, nil) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("DELETE %s: %v", url, err) } defer resp.Body.Close() var r bridgeAPIResponse mustDecodeJSON(t, resp.Body, &r) return r } func TestBridge_SessionList(t *testing.T) { _, baseURL := startTestBridgeWithREST(t, "tok") // List sessions for a new key — should create a default session r := bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1&token=tok", "") if !r.OK { t.Logf("no sessions yet: %s", r.Error) } // Create a session first r = bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{ "session_key": "test:u1:u1", "name": "work", }) if !r.OK { t.Fatalf("create session failed: %s", r.Error) } var created struct { ID string `json:"id"` Name string `json:"name"` } mustUnmarshalJSON(t, r.Data, &created) if created.ID == "" { t.Fatal("expected session ID") } if created.Name != "work" { t.Fatalf("expected name 'work', got %q", created.Name) } // Now list — should have 1 session r = bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1", "tok") if !r.OK { t.Fatalf("list sessions failed: %s", r.Error) } var listData struct { Sessions []map[string]any `json:"sessions"` } mustUnmarshalJSON(t, r.Data, &listData) if len(listData.Sessions) != 1 { t.Fatalf("expected 1 session, got %d", len(listData.Sessions)) } } func TestBridge_SessionCreateAndDetail(t *testing.T) { _, baseURL := startTestBridgeWithREST(t, "tok") // Create r := bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{ "session_key": "test:u1:u1", "name": "dev", }) if !r.OK { t.Fatalf("create failed: %s", r.Error) } var created struct { ID string `json:"id"` } mustUnmarshalJSON(t, r.Data, &created) // Get detail r = bridgeGet(t, baseURL+"/bridge/sessions/"+created.ID+"?session_key=test:u1:u1", "tok") if !r.OK { t.Fatalf("get detail failed: %s", r.Error) } var detail struct { ID string `json:"id"` Name string `json:"name"` History []map[string]any `json:"history"` } mustUnmarshalJSON(t, r.Data, &detail) if detail.ID != created.ID { t.Fatalf("expected id %q, got %q", created.ID, detail.ID) } if detail.Name != "dev" { t.Fatalf("expected name 'dev', got %q", detail.Name) } } func TestBridge_SessionDelete(t *testing.T) { _, baseURL := startTestBridgeWithREST(t, "tok") r := bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{ "session_key": "test:u1:u1", "name": "temp", }) if !r.OK { t.Fatalf("create failed: %s", r.Error) } var created struct { ID string `json:"id"` } mustUnmarshalJSON(t, r.Data, &created) // Delete r = bridgeDel(t, baseURL+"/bridge/sessions/"+created.ID+"?session_key=test:u1:u1", "tok") if !r.OK { t.Fatalf("delete failed: %s", r.Error) } // Verify deleted r = bridgeGet(t, baseURL+"/bridge/sessions/"+created.ID+"?session_key=test:u1:u1", "tok") if r.OK { t.Fatal("expected 404 after deletion") } } func TestBridge_SessionSwitch(t *testing.T) { _, baseURL := startTestBridgeWithREST(t, "tok") // Create two sessions r := bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{ "session_key": "test:u1:u1", "name": "first", }) if !r.OK { t.Fatalf("create first failed: %s", r.Error) } r = bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{ "session_key": "test:u1:u1", "name": "second", }) if !r.OK { t.Fatalf("create second failed: %s", r.Error) } var second struct { ID string `json:"id"` } mustUnmarshalJSON(t, r.Data, &second) // Switch to second r = bridgePost(t, baseURL+"/bridge/sessions/switch", "tok", map[string]string{ "session_key": "test:u1:u1", "target": second.ID, }) if !r.OK { t.Fatalf("switch failed: %s", r.Error) } var switched struct { ActiveSessionID string `json:"active_session_id"` } mustUnmarshalJSON(t, r.Data, &switched) if switched.ActiveSessionID != second.ID { t.Fatalf("expected active=%s, got %s", second.ID, switched.ActiveSessionID) } } func TestBridge_SessionAuthRequired(t *testing.T) { _, baseURL := startTestBridgeWithREST(t, "secret") r := bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1", "") if r.OK { t.Fatal("expected auth failure without token") } r = bridgeGet(t, baseURL+"/bridge/sessions?session_key=test:u1:u1", "secret") if !r.OK { t.Fatalf("expected success with token, got: %s", r.Error) } } func TestBridge_SessionMissingParams(t *testing.T) { _, baseURL := startTestBridgeWithREST(t, "tok") // Missing session_key r := bridgeGet(t, baseURL+"/bridge/sessions", "tok") if r.OK { t.Fatal("expected error without session_key") } // Missing session_key in POST r = bridgePost(t, baseURL+"/bridge/sessions", "tok", map[string]string{ "name": "test", }) if r.OK { t.Fatal("expected error without session_key in POST") } // Missing params in switch r = bridgePost(t, baseURL+"/bridge/sessions/switch", "tok", map[string]string{ "session_key": "test:u1:u1", }) if r.OK { t.Fatal("expected error without target in switch") } }