oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package callscore
2
3import (
4 "errors"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "sync/atomic"
9 "testing"
10)
11
12func TestCheckSessionReadsTheSettingsPageByStatusAlone(t *testing.T) {
13 for _, c := range []struct {
14 status int
15 want Standing
16 }{
17 {http.StatusOK, Member},
18 // requireParticipant's refusal of a guest and requireMember's of
19 // an invited address are the same 404, and both mean the same
20 // thing here: live, and not worth keeping.
21 {http.StatusNotFound, Participant},
22 } {
23 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24 if r.Method != "GET" || r.URL.Path != "/settings" {
25 t.Errorf("asked %s %s", r.Method, r.URL.Path)
26 }
27 w.WriteHeader(c.status)
28 }))
29 got, err := CheckSession(t.Context(), server.URL, server.Client())
30 if err != nil || got != c.want {
31 t.Errorf("status %d gave %q, %v", c.status, got, err)
32 }
33 server.Close()
34 }
35}
36
37func TestCheckSessionOnlyCallsTheSignInRedirectSignedOut(t *testing.T) {
38 // The whole point of the distinction: a credential is forgotten when
39 // the instance says it is gone, and kept through anything else.
40 for _, c := range []struct {
41 handler http.HandlerFunc
42 signedOut bool
43 }{
44 {func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/signin", http.StatusSeeOther) }, true},
45 {func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) }, true},
46 {func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }, false},
47 {func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadGateway) }, false},
48 } {
49 server := httptest.NewServer(c.handler)
50 _, err := CheckSession(t.Context(), server.URL, server.Client())
51 if err == nil {
52 t.Fatal("an unreadable answer was read")
53 }
54 if errors.Is(err, ErrSignedOut) != c.signedOut {
55 t.Errorf("signed out = %v, wanted %v (%v)", errors.Is(err, ErrSignedOut), c.signedOut, err)
56 }
57 server.Close()
58 }
59 // And an instance that cannot be reached at all says nothing about the
60 // session.
61 server := httptest.NewServer(http.NotFoundHandler())
62 origin := server.URL
63 server.Close()
64 if _, err := CheckSession(t.Context(), origin, &http.Client{}); err == nil || errors.Is(err, ErrSignedOut) {
65 t.Fatal("an unreachable instance signed the app out", err)
66 }
67}
68
69func TestCheckSessionNeverFollowsTheRedirect(t *testing.T) {
70 var calls atomic.Int32
71 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72 calls.Add(1)
73 http.Redirect(w, r, "/signin", http.StatusSeeOther)
74 }))
75 defer server.Close()
76 CheckSession(t.Context(), server.URL, server.Client())
77 if calls.Load() != 1 {
78 t.Fatal("followed the redirect", calls.Load())
79 }
80}
81
82func TestOpenLinkAnswersTheInstanceAsNoneWithoutAsking(t *testing.T) {
83 var calls atomic.Int32
84 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) }))
85 defer server.Close()
86 for _, address := range []string{server.URL, server.URL + "/"} {
87 got, err := OpenLink(t.Context(), server.URL, address, server.Client())
88 if err != nil || got != "" {
89 t.Errorf("%q gave %q, %v", address, got, err)
90 }
91 }
92 if calls.Load() != 0 {
93 t.Fatal("asked the server about something it did not need to", calls.Load())
94 }
95}
96
97// A /m/ address is asked about (2026-10-02): its info, with the session,
98// once, redirects not followed. Until then it was answered without
99// asking, and an app signed in as somebody without standing — a member's
100// app opened from a guest's browser — found out only when the join
101// failed.
102func TestOpenLinkAsksWhetherTheSessionMayEnterAMeeting(t *testing.T) {
103 for _, c := range []struct {
104 name string
105 handler http.HandlerFunc
106 want string
107 signedOut bool
108 noAccess bool
109 }{
110 {"may enter", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"title":"x"}`)) }, "abc123", false, false},
111 {"session gone", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/signin", http.StatusSeeOther) }, "", true, false},
112 {"not this person's meeting", func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) }, "", false, true},
113 {"server trouble", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(502) }, "", false, false},
114 } {
115 t.Run(c.name, func(t *testing.T) {
116 var asked []string
117 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 asked = append(asked, r.Method+" "+r.URL.Path)
119 c.handler(w, r)
120 }))
121 defer server.Close()
122 for _, address := range []string{server.URL + "/m/abc123", server.URL + "/m/abc123/join"} {
123 asked = nil
124 got, err := OpenLink(t.Context(), server.URL, address, server.Client())
125 if len(asked) != 1 || asked[0] != "GET /m/abc123/info" {
126 t.Fatalf("%s asked %v", address, asked)
127 }
128 if c.want != "" && (err != nil || got != c.want) {
129 t.Fatalf("%s gave %q, %v", address, got, err)
130 }
131 if c.want == "" && err == nil {
132 t.Fatalf("%s opened", address)
133 }
134 if errors.Is(err, ErrSignedOut) != c.signedOut || errors.Is(err, ErrNoAccess) != c.noAccess {
135 t.Fatalf("%s: %v", address, err)
136 }
137 }
138 })
139 }
140}
141
142func TestOpenLinkFollowsAnInvitationWithTheSessionAndReadsTheRoom(t *testing.T) {
143 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144 if r.URL.Path != "/j/token_1" {
145 t.Errorf("followed %s", r.URL.Path)
146 }
147 http.Redirect(w, r, "/m/xyz789/join", http.StatusSeeOther)
148 }))
149 defer server.Close()
150 got, err := OpenLink(t.Context(), server.URL, server.URL+"/j/token_1", server.Client())
151 if err != nil || got != "xyz789" {
152 t.Fatal(got, err)
153 }
154}
155
156func TestOpenLinkTellsAGoneSessionFromAGoneLink(t *testing.T) {
157 for _, c := range []struct {
158 handler http.HandlerFunc
159 signedOut bool
160 }{
161 // followLink with no session: the guest name form, or the sign-in
162 // page for a meeting that requires one.
163 {func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("<form>")) }, true},
164 {func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/signin", http.StatusSeeOther) }, true},
165 // A replaced link is a 404 whoever follows it.
166 {func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) }, false},
167 {func(w http.ResponseWriter, r *http.Request) {
168 http.Redirect(w, r, "https://elsewhere.test/m/abc/join", http.StatusSeeOther)
169 }, false},
170 } {
171 server := httptest.NewServer(c.handler)
172 _, err := OpenLink(t.Context(), server.URL, server.URL+"/j/token", server.Client())
173 if err == nil {
174 t.Fatal("accepted a link that did not lead to a meeting")
175 }
176 if errors.Is(err, ErrSignedOut) != c.signedOut {
177 t.Errorf("signed out = %v, wanted %v (%v)", errors.Is(err, ErrSignedOut), c.signedOut, err)
178 }
179 server.Close()
180 }
181}
182
183func TestOpenLinkRefusesAnotherInstanceBeforeSendingItTheSession(t *testing.T) {
184 var calls atomic.Int32
185 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) }))
186 defer server.Close()
187 for _, address := range []string{"https://other.test/j/token", "https://other.test/m/abc", "not a link", server.URL + "/settings"} {
188 if _, err := OpenLink(t.Context(), server.URL, address, server.Client()); err == nil {
189 t.Errorf("accepted %q", address)
190 }
191 }
192 if calls.Load() != 0 {
193 t.Fatal("a refused address still reached a server", calls.Load())
194 }
195}
196
197func TestSignOutPostsWithTheOriginAndWantsTheRedirect(t *testing.T) {
198 var origin, method, path atomic.Value
199 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
200 origin.Store(r.Header.Get("Origin"))
201 method.Store(r.Method)
202 path.Store(r.URL.Path)
203 http.Redirect(w, r, "/signed-out", http.StatusSeeOther)
204 }))
205 defer server.Close()
206 if err := SignOut(t.Context(), server.URL, server.Client()); err != nil {
207 t.Fatal(err)
208 }
209 if method.Load() != "POST" || path.Load() != "/signout" || origin.Load() != server.URL {
210 t.Fatal("wrong request", method.Load(), path.Load(), origin.Load())
211 }
212 refused := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
213 http.Error(w, "cross-origin form submission refused", http.StatusForbidden)
214 }))
215 defer refused.Close()
216 if err := SignOut(t.Context(), refused.URL, refused.Client()); err == nil || !strings.Contains(err.Error(), "did not confirm") {
217 t.Fatal("a refused sign-out was reported as done", err)
218 }
219}
220
221// In Home mode the server ends the Calls session and then answers 307 on
222// to {issuer}/signout (connect.Routes.Signout), a hop for a browser's
223// id.* session that the native client does not follow and has no use for.
224// The session is gone either way, so both redirects confirm the sign-out.
225func TestSignOutAcceptsTheHomeChain(t *testing.T) {
226 var followed atomic.Bool
227 issuer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { followed.Store(true) }))
228 defer issuer.Close()
229 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
230 http.Redirect(w, r, issuer.URL+"/signout", http.StatusTemporaryRedirect)
231 }))
232 defer server.Close()
233 if err := SignOut(t.Context(), server.URL, server.Client()); err != nil {
234 t.Fatal("a Home-mode sign-out was reported as failed:", err)
235 }
236 if followed.Load() {
237 t.Fatal("the native client followed the hop to the issuer")
238 }
239}
240