package callscore import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "amadan.net/oficina/calls/protocol" ) const testToken = "0123456789abcdef0123456789abcdef" // reportServer answers /m/room/{quality,problem} with status (and body), // recording what it was sent. type reportServer struct { *httptest.Server mu sync.Mutex paths []string tokens []string origins []string bodies []string } func newReportServer(t *testing.T, status int, body string) *reportServer { s := &reportServer{} s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { b, _ := io.ReadAll(req.Body) s.mu.Lock() s.paths = append(s.paths, req.Method+" "+req.URL.Path) s.tokens = append(s.tokens, req.Header.Get("X-Calls-Report")) s.origins = append(s.origins, req.Header.Get("Origin")) s.bodies = append(s.bodies, string(b)) s.mu.Unlock() w.WriteHeader(status) _, _ = io.WriteString(w, body) })) t.Cleanup(s.Close) return s } // helloed is a room that has been given token by a hello, as receive // would record it. func helloed(t *testing.T, srv *httptest.Server, token string) *Room { r, err := New(srv.URL, "room", srv.Client(), func(Event) {}) if err != nil { t.Fatal(err) } r.started = true // Already joined, so the hello is a resume and sends no join. r.state.Peer = "self" if err := r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self", Report: token}); err != nil { t.Fatal(err) } return r } func TestHelloReportTokenIsKeptLatestWins(t *testing.T) { srv := newReportServer(t, http.StatusAccepted, "") r := helloed(t, srv.Server, testToken) defer r.Close() if got := r.reportTokenNow(); got != testToken { t.Fatalf("token = %q", got) } // A resumed hello without one (an older hub) keeps it; a new one wins. _ = r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self"}) if got := r.reportTokenNow(); got != testToken { t.Fatalf("token after a hello without one = %q", got) } next := strings.Repeat("f", 32) _ = r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self", Report: next}) if got := r.reportTokenNow(); got != next { t.Fatalf("token after a new hello = %q", got) } } func TestSendQualityOutcomes(t *testing.T) { report := []byte(`{"v":1,"seconds":30,"client":{"app":"mac"}}`) for _, tc := range []struct { status int stop bool outcome ReportOutcome }{ {http.StatusAccepted, false, ReportSent}, {http.StatusNoContent, true, ReportSent}, {http.StatusNotFound, false, ReportGone}, {http.StatusTooManyRequests, false, ReportTooMany}, {http.StatusBadRequest, false, ReportFailed}, {http.StatusRequestEntityTooLarge, false, ReportFailed}, {http.StatusInternalServerError, false, ReportFailed}, } { t.Run(fmt.Sprint(tc.status), func(t *testing.T) { srv := newReportServer(t, tc.status, "") r := helloed(t, srv.Server, testToken) // After Close: the final report goes at leave, when the room // is already shut. r.Close() stop, err := r.SendQuality(report) if stop != tc.stop || OutcomeOf(err) != tc.outcome { t.Fatalf("stop=%v err=%v, want stop=%v %s", stop, err, tc.stop, tc.outcome) } srv.mu.Lock() defer srv.mu.Unlock() if len(srv.paths) != 1 || srv.paths[0] != "POST /m/room/quality" || srv.tokens[0] != testToken || srv.origins[0] != srv.URL || srv.bodies[0] != string(report) { t.Fatalf("request: %v %v %v %v", srv.paths, srv.tokens, srv.origins, srv.bodies) } }) } } func TestSendProblemAnswersTheReference(t *testing.T) { report := []byte(`{"note":"it growled","diagnostics":"peer abc123","client":{"app":"mac"}}`) srv := newReportServer(t, http.StatusOK, `{"ref":"K7Q2M9PD"}`) r := helloed(t, srv.Server, testToken) r.Leave() ref, err := r.SendProblem(report) if err != nil || ref != "K7Q2M9PD" { t.Fatalf("ref=%q err=%v", ref, err) } srv.mu.Lock() defer srv.mu.Unlock() last := len(srv.paths) - 1 if srv.paths[last] != "POST /m/room/problem" || srv.tokens[last] != testToken || srv.bodies[last] != string(report) { t.Fatalf("request: %v %v", srv.paths, srv.tokens) } } func TestSendProblemRefusals(t *testing.T) { report := []byte(`{"note":"","diagnostics":"","client":{"app":"mac"}}`) for _, tc := range []struct { status int body string outcome ReportOutcome }{ {http.StatusTooManyRequests, `{"error":"rate"}`, ReportTooMany}, {http.StatusNotFound, `{"error":"not-found"}`, ReportGone}, {http.StatusBadRequest, `{"error":"bad-request"}`, ReportFailed}, // A 200 that is not a reference is not shown as one. {http.StatusOK, `{"ref":"