package callscore import ( "errors" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func TestCheckSessionReadsTheSettingsPageByStatusAlone(t *testing.T) { for _, c := range []struct { status int want Standing }{ {http.StatusOK, Member}, // requireParticipant's refusal of a guest and requireMember's of // an invited address are the same 404, and both mean the same // thing here: live, and not worth keeping. {http.StatusNotFound, Participant}, } { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" || r.URL.Path != "/settings" { t.Errorf("asked %s %s", r.Method, r.URL.Path) } w.WriteHeader(c.status) })) got, err := CheckSession(t.Context(), server.URL, server.Client()) if err != nil || got != c.want { t.Errorf("status %d gave %q, %v", c.status, got, err) } server.Close() } } func TestCheckSessionOnlyCallsTheSignInRedirectSignedOut(t *testing.T) { // The whole point of the distinction: a credential is forgotten when // the instance says it is gone, and kept through anything else. for _, c := range []struct { handler http.HandlerFunc signedOut bool }{ {func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/signin", http.StatusSeeOther) }, true}, {func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) }, true}, {func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }, false}, {func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadGateway) }, false}, } { server := httptest.NewServer(c.handler) _, err := CheckSession(t.Context(), server.URL, server.Client()) if err == nil { t.Fatal("an unreadable answer was read") } if errors.Is(err, ErrSignedOut) != c.signedOut { t.Errorf("signed out = %v, wanted %v (%v)", errors.Is(err, ErrSignedOut), c.signedOut, err) } server.Close() } // And an instance that cannot be reached at all says nothing about the // session. server := httptest.NewServer(http.NotFoundHandler()) origin := server.URL server.Close() if _, err := CheckSession(t.Context(), origin, &http.Client{}); err == nil || errors.Is(err, ErrSignedOut) { t.Fatal("an unreachable instance signed the app out", err) } } func TestCheckSessionNeverFollowsTheRedirect(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) http.Redirect(w, r, "/signin", http.StatusSeeOther) })) defer server.Close() CheckSession(t.Context(), server.URL, server.Client()) if calls.Load() != 1 { t.Fatal("followed the redirect", calls.Load()) } } func TestOpenLinkAnswersTheInstanceAsNoneWithoutAsking(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) })) defer server.Close() for _, address := range []string{server.URL, server.URL + "/"} { got, err := OpenLink(t.Context(), server.URL, address, server.Client()) if err != nil || got != "" { t.Errorf("%q gave %q, %v", address, got, err) } } if calls.Load() != 0 { t.Fatal("asked the server about something it did not need to", calls.Load()) } } // A /m/ address is asked about (2026-10-02): its info, with the session, // once, redirects not followed. Until then it was answered without // asking, and an app signed in as somebody without standing — a member's // app opened from a guest's browser — found out only when the join // failed. func TestOpenLinkAsksWhetherTheSessionMayEnterAMeeting(t *testing.T) { for _, c := range []struct { name string handler http.HandlerFunc want string signedOut bool noAccess bool }{ {"may enter", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"title":"x"}`)) }, "abc123", false, false}, {"session gone", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/signin", http.StatusSeeOther) }, "", true, false}, {"not this person's meeting", func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) }, "", false, true}, {"server trouble", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(502) }, "", false, false}, } { t.Run(c.name, func(t *testing.T) { var asked []string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { asked = append(asked, r.Method+" "+r.URL.Path) c.handler(w, r) })) defer server.Close() for _, address := range []string{server.URL + "/m/abc123", server.URL + "/m/abc123/join"} { asked = nil got, err := OpenLink(t.Context(), server.URL, address, server.Client()) if len(asked) != 1 || asked[0] != "GET /m/abc123/info" { t.Fatalf("%s asked %v", address, asked) } if c.want != "" && (err != nil || got != c.want) { t.Fatalf("%s gave %q, %v", address, got, err) } if c.want == "" && err == nil { t.Fatalf("%s opened", address) } if errors.Is(err, ErrSignedOut) != c.signedOut || errors.Is(err, ErrNoAccess) != c.noAccess { t.Fatalf("%s: %v", address, err) } } }) } } func TestOpenLinkFollowsAnInvitationWithTheSessionAndReadsTheRoom(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/j/token_1" { t.Errorf("followed %s", r.URL.Path) } http.Redirect(w, r, "/m/xyz789/join", http.StatusSeeOther) })) defer server.Close() got, err := OpenLink(t.Context(), server.URL, server.URL+"/j/token_1", server.Client()) if err != nil || got != "xyz789" { t.Fatal(got, err) } } func TestOpenLinkTellsAGoneSessionFromAGoneLink(t *testing.T) { for _, c := range []struct { handler http.HandlerFunc signedOut bool }{ // followLink with no session: the guest name form, or the sign-in // page for a meeting that requires one. {func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("