oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package bridge
2
3import (
4 "encoding/base64"
5 "net/http"
6 "net/http/httptest"
7 "sync/atomic"
8 "testing"
9 "time"
10)
11
12type discardEvents struct{}
13
14func (discardEvents) OnEvent(string) {}
15func TestBridgeRequiresHumanSessionAndSafeOrigin(t *testing.T) {
16 token := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
17 cookie := `[{"name":"__Host-rastrillo_session","value":"` + token + `"}]`
18 for _, tc := range []struct{ origin, cookies string }{
19 {"https://calls.example.com", `[]`},
20 {"https://calls.example.com", `[{"name":"Authorization","value":"Bearer mt_fake"}]`},
21 {"https://calls.example.com", `[{"name":"__Host-rastrillo_session","value":"bad"}]`},
22 {"http://calls.example.com", cookie},
23 {"https://calls.example.com/path", cookie},
24 } {
25 if c, err := NewClient(tc.origin, "room", tc.cookies, discardEvents{}); err == nil {
26 c.Close()
27 t.Fatal("accepted invalid native session")
28 }
29 }
30 c, err := NewClient("https://calls.example.com", "room", cookie, discardEvents{})
31 if err != nil {
32 t.Fatal(err)
33 }
34 c.Close()
35}
36
37// loopbackSession is a session cookie as the handoff returns it for an
38// http://127.0.0.1 instance, which is the one place a test server can
39// be and the bridge will still send it.
40func loopbackSession() (string, string) {
41 token := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
42 return token, `[{"name":"rastrillo_session","value":"` + token + `"}]`
43}
44
45func TestCheckSessionCarriesTheCookieAndAnswersAValue(t *testing.T) {
46 token, cookies := loopbackSession()
47 status := http.StatusOK
48 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49 if c, err := r.Cookie("rastrillo_session"); err != nil || c.Value != token {
50 t.Error("session not sent")
51 }
52 if status == http.StatusSeeOther {
53 http.Redirect(w, r, "/signin", status)
54 return
55 }
56 w.WriteHeader(status)
57 }))
58 defer server.Close()
59 for s, want := range map[int]string{http.StatusOK: "member", http.StatusNotFound: "participant", http.StatusSeeOther: SignedOut} {
60 status = s
61 got, err := CheckSession(server.URL, cookies)
62 if err != nil || got != want {
63 t.Errorf("status %d gave %q, %v", s, got, err)
64 }
65 }
66 status = http.StatusBadGateway
67 if got, err := CheckSession(server.URL, cookies); err == nil || got == SignedOut {
68 t.Fatal("a bad gateway signed the app out", got, err)
69 }
70}
71
72func TestCheckSessionCallsAStoredCopyItCannotSendSignedOut(t *testing.T) {
73 var calls atomic.Int32
74 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) }))
75 defer server.Close()
76 for _, cookies := range []string{``, `[]`, `not json`, `[{"name":"rastrillo_session","value":"short"}]`} {
77 if got, err := CheckSession(server.URL, cookies); err != nil || got != SignedOut {
78 t.Errorf("%q gave %q, %v", cookies, got, err)
79 }
80 }
81 if calls.Load() != 0 {
82 t.Fatal("sent a session it should have refused", calls.Load())
83 }
84}
85
86func TestOpenLinkKeepsNoMeetingAndNoSessionApart(t *testing.T) {
87 _, cookies := loopbackSession()
88 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
89 switch r.URL.Path {
90 case "/j/live":
91 http.Redirect(w, r, "/m/abc123/join", http.StatusSeeOther)
92 case "/j/gone":
93 http.Redirect(w, r, "/signin", http.StatusSeeOther)
94 case "/m/xyz99/info":
95 w.Write([]byte(`{"title":"x"}`))
96 default:
97 http.NotFound(w, r)
98 }
99 }))
100 defer server.Close()
101 for address, want := range map[string]string{
102 server.URL: `{"room":""}`,
103 server.URL + "/j/live": `{"room":"abc123"}`,
104 server.URL + "/j/gone": `{"room":"","signedOut":true}`,
105 server.URL + "/m/xyz99": `{"room":"xyz99"}`,
106 // Signed in, as somebody this meeting is not for (2026-10-02).
107 server.URL + "/m/other1": `{"room":"","noAccess":true}`,
108 } {
109 got, err := OpenLink(server.URL, cookies, address)
110 if err != nil || got != want {
111 t.Errorf("%q gave %s, %v", address, got, err)
112 }
113 }
114 if _, err := OpenLink(server.URL, cookies, server.URL+"/j/replaced"); err == nil {
115 t.Fatal("a replaced link opened")
116 }
117}
118
119// TestReportsCarryTheHellosTokenAfterLeave is spec §15.26 through the
120// bridge: the token from the stream's hello goes on both report requests
121// with the session, the answers come back as values the host can act on,
122// and both still work once the call has been left.
123func TestReportsCarryTheHellosTokenAfterLeave(t *testing.T) {
124 const report = "0123456789abcdef0123456789abcdef"
125 session, cookies := loopbackSession()
126 var quality, problem atomic.Int32
127 hello := make(chan struct{})
128 var once atomic.Bool
129 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
130 if c, err := r.Cookie("rastrillo_session"); err != nil || c.Value != session {
131 t.Error("session not sent")
132 }
133 switch r.URL.Path {
134 case "/m/room/stream":
135 w.Header().Set("Content-Type", "text/event-stream")
136 w.Write([]byte("data: {\"type\":\"hello\",\"peer\":\"self\",\"report\":\"" + report + "\"}\n\n"))
137 w.(http.Flusher).Flush()
138 if once.CompareAndSwap(false, true) {
139 close(hello)
140 }
141 <-r.Context().Done()
142 case "/m/room/quality":
143 if r.Header.Get("X-Calls-Report") != report {
144 t.Error("quality without the token")
145 }
146 if quality.Add(1) == 1 {
147 w.WriteHeader(http.StatusAccepted)
148 } else {
149 w.WriteHeader(http.StatusNoContent)
150 }
151 case "/m/room/problem":
152 if r.Header.Get("X-Calls-Report") != report {
153 t.Error("problem without the token")
154 }
155 if problem.Add(1) == 1 {
156 w.Write([]byte(`{"ref":"K7Q2M9PD"}`))
157 } else {
158 w.WriteHeader(http.StatusTooManyRequests)
159 }
160 case "/m/room/info":
161 w.Write([]byte(`{"title":"x"}`))
162 default:
163 // The join, and the leave.
164 w.WriteHeader(http.StatusOK)
165 }
166 }))
167 defer server.Close()
168 c, err := NewClient(server.URL, "room", cookies, discardEvents{})
169 if err != nil {
170 t.Fatal(err)
171 }
172 if got := c.SendQuality(`{}`); got != "gone" {
173 t.Fatalf("before the hello: %q", got)
174 }
175 if err := c.Start(); err != nil {
176 t.Fatal(err)
177 }
178 <-hello
179 // The hello is applied after it is read; give the core its turn.
180 for i := 0; i < 100 && c.SendQuality(`{"v":1}`) == "gone"; i++ {
181 time.Sleep(20 * time.Millisecond)
182 }
183 c.Leave()
184 if got := c.SendQuality(`{"v":1,"final":true}`); got != "stop" {
185 t.Fatalf("after leave, the kill switch: %q", got)
186 }
187 if got := c.SendProblem(`{"note":"x"}`); got != `{"ref":"K7Q2M9PD"}` {
188 t.Fatalf("problem: %s", got)
189 }
190 if got := c.SendProblem(`{"note":"x"}`); got != `{"outcome":"too-many"}` {
191 t.Fatalf("second problem: %s", got)
192 }
193}
194