package callscore import ( "encoding/json" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func TestStartMeetingSendsBothLocksAndAnswersTheSqid(t *testing.T) { var origin, contentType, method, path atomic.Value server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin.Store(r.Header.Get("Origin")) contentType.Store(r.Header.Get("Content-Type")) method.Store(r.Method) path.Store(r.URL.Path) json.NewEncoder(w).Encode(map[string]string{"sqid": "abc123", "title": "Call, 17 Sep 11:23"}) })) defer server.Close() sqid, err := StartMeeting(t.Context(), server.URL, server.Client()) if err != nil { t.Fatal(err) } if sqid != "abc123" { t.Fatal("wrong meeting", sqid) } if method.Load() != "POST" || path.Load() != "/native/meetings/start" { t.Fatal("wrong request", method.Load(), path.Load()) } // csrf.Protect refuses a POST that carries neither, and it is worth a // test because the failure is a 403 on a live server and nothing a // unit test of the handler would ever see. if origin.Load() != server.URL { t.Fatal("origin not sent", origin.Load()) } if contentType.Load() != "application/json" { t.Fatal("content type not sent", contentType.Load()) } } func TestStartMeetingRefusesAnAnswerItCannotOpen(t *testing.T) { for _, body := range []string{`{"sqid":""}`, `{"sqid":"../admin"}`, `{"sqid":"has space"}`, `{"sqid":"` + strings.Repeat("a", 33) + `"}`, `not json`, `{}`} { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(body)) })) if _, err := StartMeeting(t.Context(), server.URL, server.Client()); err == nil { t.Errorf("accepted %q", body) } server.Close() } } func TestStartMeetingNeverFollowsARedirectWithTheSession(t *testing.T) { // A session that went comes back as the sign-in page's redirect. Follow // it and the app carries a credential to wherever it points, then // decodes a page as JSON. 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() _, err := StartMeeting(t.Context(), server.URL, server.Client()) if err == nil { t.Fatal("redirect accepted") } if !strings.Contains(err.Error(), "sign in") { t.Fatal("unhelpful error", err) } if calls.Load() != 1 { t.Fatal("followed the redirect", calls.Load()) } } func TestStartMeetingRefusesAnUnsafeOriginBeforeSendingAnything(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 _, origin := range []string{"http://example.test", "https://alice:secret@example.test", "https://example.test?x=1", "https://example.test/m/abc", "", "not a url"} { if _, err := StartMeeting(t.Context(), origin, server.Client()); err == nil { t.Errorf("accepted %q", origin) } } if calls.Load() != 0 { t.Fatal("a refused origin still reached a server", calls.Load()) } if _, err := StartMeeting(t.Context(), server.URL, nil); err == nil { t.Fatal("accepted a client with no session") } } func TestStartMeetingNamesTheRefusalItCanDoSomethingAbout(t *testing.T) { for _, c := range []struct { status int want string }{ {http.StatusUnauthorized, "sign in"}, {http.StatusForbidden, "cannot start meetings"}, {http.StatusNotFound, "cannot start meetings"}, {http.StatusInternalServerError, "just now"}, } { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "no", c.status) })) _, err := StartMeeting(t.Context(), server.URL, server.Client()) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("status %d gave %v, wanted something about %q", c.status, err, c.want) } server.Close() } } func TestRegisterDeviceSendsTheTokenAndBothLocks(t *testing.T) { var body, origin, contentType atomic.Value server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin.Store(r.Header.Get("Origin")) contentType.Store(r.Header.Get("Content-Type")) var in map[string]string json.NewDecoder(r.Body).Decode(&in) body.Store(in) w.WriteHeader(http.StatusNoContent) })) defer server.Close() if err := RegisterDevice(t.Context(), server.URL, server.Client(), "aabbcc", "macos"); err != nil { t.Fatal(err) } in := body.Load().(map[string]string) if in["token"] != "aabbcc" || in["platform"] != "macos" { t.Fatal("wrong body", in) } if origin.Load() != server.URL || contentType.Load() != "application/json" { t.Fatal("missing a lock", origin.Load(), contentType.Load()) } } func TestRegisterDeviceRefusesAnUnknownPlatformWithoutCallingTheServer(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() if err := RegisterDevice(t.Context(), server.URL, server.Client(), "aabb", "android"); err == nil { t.Fatal("accepted an unknown platform") } if calls.Load() != 0 { t.Fatal("called the server anyway") } } func TestRegisterDeviceOnAnOlderServerIsAPlainRefusal(t *testing.T) { // A server without the endpoint has no push. That is a fact about // the server, not a failure worth showing anyone. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) })) defer server.Close() err := RegisterDevice(t.Context(), server.URL, server.Client(), "aabb", "ios") if err == nil || !strings.Contains(err.Error(), "does not take device registrations") { t.Fatal("unhelpful error", err) } }