| 1 | package callscore |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "strings" |
| 10 | "sync" |
| 11 | "testing" |
| 12 | |
| 13 | "amadan.net/oficina/calls/protocol" |
| 14 | ) |
| 15 | |
| 16 | const testToken = "0123456789abcdef0123456789abcdef" |
| 17 | |
| 18 | // reportServer answers /m/room/{quality,problem} with status (and body), |
| 19 | // recording what it was sent. |
| 20 | type reportServer struct { |
| 21 | *httptest.Server |
| 22 | mu sync.Mutex |
| 23 | paths []string |
| 24 | tokens []string |
| 25 | origins []string |
| 26 | bodies []string |
| 27 | } |
| 28 | |
| 29 | func newReportServer(t *testing.T, status int, body string) *reportServer { |
| 30 | s := &reportServer{} |
| 31 | s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| 32 | b, _ := io.ReadAll(req.Body) |
| 33 | s.mu.Lock() |
| 34 | s.paths = append(s.paths, req.Method+" "+req.URL.Path) |
| 35 | s.tokens = append(s.tokens, req.Header.Get("X-Calls-Report")) |
| 36 | s.origins = append(s.origins, req.Header.Get("Origin")) |
| 37 | s.bodies = append(s.bodies, string(b)) |
| 38 | s.mu.Unlock() |
| 39 | w.WriteHeader(status) |
| 40 | _, _ = io.WriteString(w, body) |
| 41 | })) |
| 42 | t.Cleanup(s.Close) |
| 43 | return s |
| 44 | } |
| 45 | |
| 46 | // helloed is a room that has been given token by a hello, as receive |
| 47 | // would record it. |
| 48 | func helloed(t *testing.T, srv *httptest.Server, token string) *Room { |
| 49 | r, err := New(srv.URL, "room", srv.Client(), func(Event) {}) |
| 50 | if err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | r.started = true |
| 54 | // Already joined, so the hello is a resume and sends no join. |
| 55 | r.state.Peer = "self" |
| 56 | if err := r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self", Report: token}); err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | return r |
| 60 | } |
| 61 | |
| 62 | func TestHelloReportTokenIsKeptLatestWins(t *testing.T) { |
| 63 | srv := newReportServer(t, http.StatusAccepted, "") |
| 64 | r := helloed(t, srv.Server, testToken) |
| 65 | defer r.Close() |
| 66 | if got := r.reportTokenNow(); got != testToken { |
| 67 | t.Fatalf("token = %q", got) |
| 68 | } |
| 69 | // A resumed hello without one (an older hub) keeps it; a new one wins. |
| 70 | _ = r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self"}) |
| 71 | if got := r.reportTokenNow(); got != testToken { |
| 72 | t.Fatalf("token after a hello without one = %q", got) |
| 73 | } |
| 74 | next := strings.Repeat("f", 32) |
| 75 | _ = r.receive(protocol.Message{Type: protocol.TypeHello, Peer: "self", Report: next}) |
| 76 | if got := r.reportTokenNow(); got != next { |
| 77 | t.Fatalf("token after a new hello = %q", got) |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func TestSendQualityOutcomes(t *testing.T) { |
| 82 | report := []byte(`{"v":1,"seconds":30,"client":{"app":"mac"}}`) |
| 83 | for _, tc := range []struct { |
| 84 | status int |
| 85 | stop bool |
| 86 | outcome ReportOutcome |
| 87 | }{ |
| 88 | {http.StatusAccepted, false, ReportSent}, |
| 89 | {http.StatusNoContent, true, ReportSent}, |
| 90 | {http.StatusNotFound, false, ReportGone}, |
| 91 | {http.StatusTooManyRequests, false, ReportTooMany}, |
| 92 | {http.StatusBadRequest, false, ReportFailed}, |
| 93 | {http.StatusRequestEntityTooLarge, false, ReportFailed}, |
| 94 | {http.StatusInternalServerError, false, ReportFailed}, |
| 95 | } { |
| 96 | t.Run(fmt.Sprint(tc.status), func(t *testing.T) { |
| 97 | srv := newReportServer(t, tc.status, "") |
| 98 | r := helloed(t, srv.Server, testToken) |
| 99 | // After Close: the final report goes at leave, when the room |
| 100 | // is already shut. |
| 101 | r.Close() |
| 102 | stop, err := r.SendQuality(report) |
| 103 | if stop != tc.stop || OutcomeOf(err) != tc.outcome { |
| 104 | t.Fatalf("stop=%v err=%v, want stop=%v %s", stop, err, tc.stop, tc.outcome) |
| 105 | } |
| 106 | srv.mu.Lock() |
| 107 | defer srv.mu.Unlock() |
| 108 | if len(srv.paths) != 1 || srv.paths[0] != "POST /m/room/quality" || srv.tokens[0] != testToken || |
| 109 | srv.origins[0] != srv.URL || srv.bodies[0] != string(report) { |
| 110 | t.Fatalf("request: %v %v %v %v", srv.paths, srv.tokens, srv.origins, srv.bodies) |
| 111 | } |
| 112 | }) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | func TestSendProblemAnswersTheReference(t *testing.T) { |
| 117 | report := []byte(`{"note":"it growled","diagnostics":"peer abc123","client":{"app":"mac"}}`) |
| 118 | srv := newReportServer(t, http.StatusOK, `{"ref":"K7Q2M9PD"}`) |
| 119 | r := helloed(t, srv.Server, testToken) |
| 120 | r.Leave() |
| 121 | ref, err := r.SendProblem(report) |
| 122 | if err != nil || ref != "K7Q2M9PD" { |
| 123 | t.Fatalf("ref=%q err=%v", ref, err) |
| 124 | } |
| 125 | srv.mu.Lock() |
| 126 | defer srv.mu.Unlock() |
| 127 | last := len(srv.paths) - 1 |
| 128 | if srv.paths[last] != "POST /m/room/problem" || srv.tokens[last] != testToken || srv.bodies[last] != string(report) { |
| 129 | t.Fatalf("request: %v %v", srv.paths, srv.tokens) |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func TestSendProblemRefusals(t *testing.T) { |
| 134 | report := []byte(`{"note":"","diagnostics":"","client":{"app":"mac"}}`) |
| 135 | for _, tc := range []struct { |
| 136 | status int |
| 137 | body string |
| 138 | outcome ReportOutcome |
| 139 | }{ |
| 140 | {http.StatusTooManyRequests, `{"error":"rate"}`, ReportTooMany}, |
| 141 | {http.StatusNotFound, `{"error":"not-found"}`, ReportGone}, |
| 142 | {http.StatusBadRequest, `{"error":"bad-request"}`, ReportFailed}, |
| 143 | // A 200 that is not a reference is not shown as one. |
| 144 | {http.StatusOK, `{"ref":"<script>"}`, ReportFailed}, |
| 145 | {http.StatusOK, `not json`, ReportFailed}, |
| 146 | } { |
| 147 | t.Run(fmt.Sprint(tc.status, tc.body), func(t *testing.T) { |
| 148 | srv := newReportServer(t, tc.status, tc.body) |
| 149 | r := helloed(t, srv.Server, testToken) |
| 150 | defer r.Close() |
| 151 | ref, err := r.SendProblem(report) |
| 152 | if ref != "" || OutcomeOf(err) != tc.outcome { |
| 153 | t.Fatalf("ref=%q err=%v, want %s", ref, err, tc.outcome) |
| 154 | } |
| 155 | }) |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | func TestReportsNeedATokenAndABoundedBody(t *testing.T) { |
| 160 | srv := newReportServer(t, http.StatusAccepted, "") |
| 161 | // No token: a server from before §15.26 never gave one. Nothing is sent. |
| 162 | r := helloed(t, srv.Server, "") |
| 163 | defer r.Close() |
| 164 | if _, err := r.SendQuality([]byte(`{}`)); OutcomeOf(err) != ReportGone { |
| 165 | t.Fatalf("no token: %v", err) |
| 166 | } |
| 167 | if _, err := r.SendProblem([]byte(`{}`)); OutcomeOf(err) != ReportGone { |
| 168 | t.Fatalf("no token: %v", err) |
| 169 | } |
| 170 | r.mu.Lock() |
| 171 | r.report = testToken |
| 172 | r.mu.Unlock() |
| 173 | big, _ := json.Marshal(map[string]string{"x": strings.Repeat("a", protocol.MaxQualityBody)}) |
| 174 | if _, err := r.SendQuality(big); OutcomeOf(err) != ReportFailed { |
| 175 | t.Fatalf("oversized quality: %v", err) |
| 176 | } |
| 177 | if _, err := r.SendQuality([]byte(`{not json`)); OutcomeOf(err) != ReportFailed { |
| 178 | t.Fatalf("invalid quality: %v", err) |
| 179 | } |
| 180 | huge, _ := json.Marshal(map[string]string{"x": strings.Repeat("a", protocol.MaxProblemBody)}) |
| 181 | if _, err := r.SendProblem(huge); OutcomeOf(err) != ReportFailed { |
| 182 | t.Fatalf("oversized problem: %v", err) |
| 183 | } |
| 184 | srv.mu.Lock() |
| 185 | defer srv.mu.Unlock() |
| 186 | if len(srv.paths) != 0 { |
| 187 | t.Fatalf("sent anyway: %v", srv.paths) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | func TestReportNeverFollowsARedirect(t *testing.T) { |
| 192 | reached := false |
| 193 | foreign := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true })) |
| 194 | defer foreign.Close() |
| 195 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| 196 | http.Redirect(w, req, foreign.URL, http.StatusTemporaryRedirect) |
| 197 | })) |
| 198 | defer srv.Close() |
| 199 | r := helloed(t, srv, testToken) |
| 200 | defer r.Close() |
| 201 | if _, err := r.SendProblem([]byte(`{}`)); OutcomeOf(err) != ReportFailed || reached { |
| 202 | t.Fatalf("err=%v reached=%v", err, reached) |
| 203 | } |
| 204 | } |
| 205 | |