| 1 | package calls |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "net/url" |
| 10 | "regexp" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | ) |
| 15 | |
| 16 | func TestHomeConnectionKeepsCurrentCallAndAgentRoutes(t *testing.T) { |
| 17 | f := newHomeRouterFixture(t) |
| 18 | f.link(t) |
| 19 | started := f.do("POST", "/meetings/start", "") |
| 20 | if started.Code != http.StatusOK { |
| 21 | t.Fatalf("start call: %d", started.Code) |
| 22 | } |
| 23 | links := regexp.MustCompile(`href="(/m/[A-Za-z0-9]+/join)"`).FindStringSubmatch(started.Body.String()) |
| 24 | if len(links) != 2 { |
| 25 | t.Fatal("missing holding page link") |
| 26 | } |
| 27 | target := links[1] |
| 28 | if !strings.HasPrefix(target, "/m/") { |
| 29 | t.Fatal("start call lost room destination") |
| 30 | } |
| 31 | if w := f.do("GET", target, ""); w.Code != 200 { |
| 32 | t.Fatalf("call page: %d", w.Code) |
| 33 | } |
| 34 | room := strings.TrimSuffix(target, "/join") |
| 35 | if w := f.do("GET", room+"/stream?probe=1", ""); w.Code != http.StatusNoContent { |
| 36 | t.Fatalf("Home stream probe: %d", w.Code) |
| 37 | } |
| 38 | server := httptest.NewServer(f.handler) |
| 39 | defer server.Close() |
| 40 | ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) |
| 41 | defer cancel() |
| 42 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL+room+"/stream", nil) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | origin, _ := url.Parse(homeTestOrigin) |
| 47 | req.Host = origin.Host |
| 48 | req.Header.Set("Accept", "text/event-stream") |
| 49 | for _, c := range f.cookies { |
| 50 | req.AddCookie(c) |
| 51 | } |
| 52 | resp, err := server.Client().Do(req) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | defer resp.Body.Close() |
| 57 | if resp.StatusCode != 200 { |
| 58 | t.Fatalf("Home stream: %d", resp.StatusCode) |
| 59 | } |
| 60 | scanner := bufio.NewScanner(resp.Body) |
| 61 | var hello struct{ Peer string } |
| 62 | for scanner.Scan() { |
| 63 | if strings.HasPrefix(scanner.Text(), "data:") { |
| 64 | if err := json.Unmarshal([]byte(strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "data:"))), &hello); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | if hello.Peer != "" { |
| 68 | break |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | if hello.Peer == "" { |
| 73 | t.Fatalf("Home stream missing hello: %v", scanner.Err()) |
| 74 | } |
| 75 | payload, err := json.Marshal(map[string]string{"type": "join", "peer": hello.Peer}) |
| 76 | if err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | signal := httptest.NewRequest("POST", homeTestOrigin+room+"/signal", strings.NewReader(string(payload))) |
| 80 | signal.Header.Set("Origin", homeTestOrigin) |
| 81 | signal.Header.Set("Content-Type", "application/json") |
| 82 | for _, c := range f.cookies { |
| 83 | signal.AddCookie(c) |
| 84 | } |
| 85 | joined := httptest.NewRecorder() |
| 86 | f.handler.ServeHTTP(joined, signal) |
| 87 | if joined.Code != 200 { |
| 88 | t.Fatalf("Home join signal: %d", joined.Code) |
| 89 | } |
| 90 | if w := f.do("GET", room+"/ice?peer="+hello.Peer, ""); w.Code != 200 { |
| 91 | t.Fatalf("Home admitted ICE: %d", w.Code) |
| 92 | } |
| 93 | resp.Body.Close() |
| 94 | cancel() |
| 95 | if w := f.do("GET", "/settings", ""); w.Code != 200 || !strings.Contains(w.Body.String(), "alice@example.test") { |
| 96 | t.Fatalf("settings lost existing principal: %d", w.Code) |
| 97 | } |
| 98 | created := f.do("POST", "/settings/agents", "name=home-integration-test") |
| 99 | if created.Code != 200 { |
| 100 | t.Fatalf("create agent: %d", created.Code) |
| 101 | } |
| 102 | token := regexp.MustCompile(`value="(mt_[A-Za-z0-9_-]+)"`).FindStringSubmatch(created.Body.String()) |
| 103 | if len(token) != 2 { |
| 104 | t.Fatal("missing once-shown agent token") |
| 105 | } |
| 106 | api := func(method, path, body, bearer string, cookies bool) *httptest.ResponseRecorder { |
| 107 | r := httptest.NewRequest(method, homeTestOrigin+path, strings.NewReader(body)) |
| 108 | r.Header.Set("Content-Type", "application/json") |
| 109 | if bearer != "" { |
| 110 | r.Header.Set("Authorization", "Bearer "+bearer) |
| 111 | } |
| 112 | if cookies { |
| 113 | for _, c := range f.cookies { |
| 114 | r.AddCookie(c) |
| 115 | } |
| 116 | } |
| 117 | w := httptest.NewRecorder() |
| 118 | f.handler.ServeHTTP(w, r) |
| 119 | return w |
| 120 | } |
| 121 | if w := api("GET", "/api/v1/meetings", "", token[1], false); w.Code != 200 { |
| 122 | t.Fatalf("bearer API with Home enabled: %d", w.Code) |
| 123 | } |
| 124 | if w := api("POST", "/api/v1/meetings", `{"title":"Agent call"}`, token[1], false); w.Code != 201 { |
| 125 | t.Fatalf("bearer mutation incorrectly requires browser proof: %d", w.Code) |
| 126 | } |
| 127 | if w := api("GET", "/api/v1/meetings", "", "", true); w.Code != 401 { |
| 128 | t.Fatalf("API accepted ambient Home cookie: %d", w.Code) |
| 129 | } |
| 130 | r := httptest.NewRequest("POST", homeTestOrigin+"/meetings/start", nil) |
| 131 | for _, c := range f.cookies { |
| 132 | r.AddCookie(c) |
| 133 | } |
| 134 | w := httptest.NewRecorder() |
| 135 | f.handler.ServeHTTP(w, r) |
| 136 | if w.Code != 403 { |
| 137 | t.Fatalf("browser mutation lost CSRF protection: %d", w.Code) |
| 138 | } |
| 139 | homeSignout(t, f.do("POST", "/signout", "")) |
| 140 | if w := f.do("GET", target, ""); w.Code != 303 { |
| 141 | t.Fatalf("room survives signout: %d", w.Code) |
| 142 | } |
| 143 | } |
| 144 | |