package web import ( "bytes" "context" "encoding/json" "errors" "io/fs" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync" "testing" "testing/fstest" "time" "speedtest/internal/probe" recordstore "speedtest/internal/store" ) func TestRecordsEndpointReturnsRecords(t *testing.T) { store := &memoryStore{ records: []recordstore.Record{ { ID: "run-2", Client: recordstore.ClientInfo{IP: "203.0.113.20", ISP: "Example Mobile", Region: "Beijing"}, Summary: probe.Summary{ID: "run-2", Target: "example.org:443", Score: 88}, Samples: []probe.Sample{{Kind: "tcp", Success: true, LatencyMS: 40}}, }, { ID: "run-1", Client: recordstore.ClientInfo{IP: "198.51.100.10", ISP: "Example Telecom", Region: "Shanghai"}, Summary: probe.Summary{ID: "run-1", Target: "example.com:80", Score: 95}, Samples: []probe.Sample{{Kind: "tcp", Success: true, LatencyMS: 30}}, }, }, } server := New(Options{Store: store}) req := httptest.NewRequest(http.MethodGet, "/api/records?limit=2", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } var payload recordsResponse if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } if len(payload.Records) != 2 { t.Fatalf("len(records) = %d, want 2", len(payload.Records)) } if payload.Records[0].ID != "run-2" { t.Fatalf("first record id = %q, want run-2", payload.Records[0].ID) } if payload.Records[0].Client.IP != "203.0.113.*" || payload.Records[0].Client.ISP != "Example Mobile" { t.Fatalf("first record client = %#v, want anonymized client data", payload.Records[0].Client) } if len(payload.Records[0].Samples) != 1 { t.Fatalf("first record samples = %d, want 1", len(payload.Records[0].Samples)) } } func TestRecordsEndpointAnonymizesClientIPAndFillsMissingISP(t *testing.T) { store := &memoryStore{ records: []recordstore.Record{ { ID: "run-1", Client: recordstore.ClientInfo{IP: "203.0.113.20", Region: "Beijing"}, Summary: probe.Summary{ID: "run-1", Target: "example.com:80", Score: 95}, }, }, } server := New(Options{Store: store}) req := httptest.NewRequest(http.MethodGet, "/api/records?limit=1", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } var payload recordsResponse if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } client := payload.Records[0].Client if client.IP != "203.0.113.*" { t.Fatalf("client IP = %q, want anonymized 203.0.113.*", client.IP) } if client.ISP != "未知运营商" { t.Fatalf("client ISP = %q, want 未知运营商", client.ISP) } } func TestConfigEndpointReturnsBandwidthLimit(t *testing.T) { server := New(Options{Store: &memoryStore{}, BandwidthLimitMbps: 80}) req := httptest.NewRequest(http.MethodGet, "/api/config", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } var payload configResponse if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.BandwidthLimitMbps != 80 { t.Fatalf("BandwidthLimitMbps = %.2f, want 80", payload.BandwidthLimitMbps) } if payload.BandwidthLimitLabel != "80 Mbps" { t.Fatalf("BandwidthLimitLabel = %q, want 80 Mbps", payload.BandwidthLimitLabel) } } func TestHealthEventsStreamsHeartbeat(t *testing.T) { server := New(Options{Store: &memoryStore{}, BandwidthLimitMbps: 30}) req := httptest.NewRequest(http.MethodGet, "/api/health/events?limit=1", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } if contentType := res.Header().Get("Content-Type"); !strings.HasPrefix(contentType, "text/event-stream") { t.Fatalf("Content-Type = %q, want text/event-stream", contentType) } body := res.Body.String() if !strings.Contains(body, "event: heartbeat") || !strings.Contains(body, `"bandwidthLimitLabel":"30 Mbps"`) { t.Fatalf("heartbeat body = %q, want heartbeat with bandwidth label", body) } } func TestDownloadEndpointStreamsRequestedBytesAndAdvertisesLimit(t *testing.T) { server := New(Options{Store: &memoryStore{}, BandwidthLimitMbps: 80}) createReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":1}`)) createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createTestResponse if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } req := httptest.NewRequest(http.MethodGet, created.DownloadURL+"&bytes=1024", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } if res.Body.Len() != 1024 { t.Fatalf("download length = %d, want 1024", res.Body.Len()) } if got := res.Header().Get("X-NetStable-Bandwidth-Limit-Mbps"); got != "80" { t.Fatalf("limit header = %q, want 80", got) } } func TestDownloadEndpointRequiresActiveTestID(t *testing.T) { server := New(Options{Store: &memoryStore{}}) req := httptest.NewRequest(http.MethodGet, "/api/download?bytes=1024", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusConflict { t.Fatalf("status = %d, want 409; body=%s", res.Code, res.Body.String()) } } func TestUploadEndpointAcceptsActiveSession(t *testing.T) { server := New(Options{Store: &memoryStore{}, BandwidthLimitMbps: 80}) createReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":1}`)) createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createTestResponse if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } req := httptest.NewRequest(http.MethodPost, created.UploadURL, bytes.NewBuffer(make([]byte, 2048))) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } var payload transferResponse if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("decode upload response: %v", err) } if payload.Bytes != 2048 { t.Fatalf("uploaded bytes = %d, want 2048", payload.Bytes) } } func TestUploadEndpointRequiresActiveTestID(t *testing.T) { server := New(Options{Store: &memoryStore{}}) req := httptest.NewRequest(http.MethodPost, "/api/upload?bytes=1024", bytes.NewBuffer(make([]byte, 1024))) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusConflict { t.Fatalf("status = %d, want 409; body=%s", res.Code, res.Body.String()) } } func TestDownloadEndpointRejectsInvalidByteCount(t *testing.T) { server := New(Options{Store: &memoryStore{}}) req := httptest.NewRequest(http.MethodGet, "/api/download?bytes=0", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400; body=%s", res.Code, res.Body.String()) } } func TestCreateTestRejectsInvalidTarget(t *testing.T) { server := New(Options{Store: &memoryStore{}}) req := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com:abc"}`)) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400; body=%s", res.Code, res.Body.String()) } if !strings.Contains(res.Body.String(), "invalid target") { t.Fatalf("body = %q, want invalid target error", res.Body.String()) } } func TestClientIPUsesRemoteAddrUnlessProxyHeadersAreTrusted(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "198.51.100.10:54321" req.Header.Set("X-Forwarded-For", "203.0.113.20") req.Header.Set("X-Real-IP", "203.0.113.30") if got := clientIPFromRequest(req, false); got != "198.51.100.10" { t.Fatalf("clientIPFromRequest untrusted = %q, want RemoteAddr IP", got) } if got := clientIPFromRequest(req, true); got != "203.0.113.30" { t.Fatalf("clientIPFromRequest trusted = %q, want X-Real-IP", got) } } func TestCreateTestReturnsSessionIDAndRouteURLs(t *testing.T) { server := New(Options{Store: &memoryStore{}, ClientResolver: staticResolver}) req := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":1,"intervalMillis":200,"timeoutMillis":300}`)) req.RemoteAddr = "198.51.100.10:54321" res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusAccepted { t.Fatalf("status = %d, want 202; body=%s", res.Code, res.Body.String()) } var payload createTestResponse if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.ID == "" { t.Fatal("payload.ID is empty") } if payload.Status != "ready" || payload.QueuePosition != 0 || payload.QueueURL != "/api/tests/"+payload.ID+"/queue" { t.Fatalf("queue fields = %q/%d/%q, want ready/0/queue URL", payload.Status, payload.QueuePosition, payload.QueueURL) } if payload.DownloadURL != "/api/download?testId="+payload.ID { t.Fatalf("DownloadURL = %q, want /api/download?testId=%s", payload.DownloadURL, payload.ID) } if payload.UploadURL != "/api/upload?testId="+payload.ID { t.Fatalf("UploadURL = %q, want /api/upload?testId=%s", payload.UploadURL, payload.ID) } if payload.CompleteURL != "/api/tests/"+payload.ID+"/complete" { t.Fatalf("CompleteURL = %q, want /api/tests/%s/complete", payload.CompleteURL, payload.ID) } if payload.CancelURL != "/api/tests/"+payload.ID+"/cancel" { t.Fatalf("CancelURL = %q, want /api/tests/%s/cancel", payload.CancelURL, payload.ID) } } func TestCreateTestQueuesWhenAnotherSessionIsActive(t *testing.T) { server := New(Options{Store: &memoryStore{}}) firstReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":1}`)) firstRes := httptest.NewRecorder() server.Handler().ServeHTTP(firstRes, firstReq) if firstRes.Code != http.StatusAccepted { t.Fatalf("first status = %d, want 202; body=%s", firstRes.Code, firstRes.Body.String()) } secondReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondReq) if secondRes.Code != http.StatusAccepted { t.Fatalf("second status = %d, want 202 queued; body=%s", secondRes.Code, secondRes.Body.String()) } var queued createTestResponse if err := json.Unmarshal(secondRes.Body.Bytes(), &queued); err != nil { t.Fatalf("decode queued response: %v", err) } if queued.Status != "queued" || queued.QueuePosition != 1 || queued.QueueURL != "/api/tests/"+queued.ID+"/queue" { t.Fatalf("queued response = %#v, want queued at position 1", queued) } } func TestQueuedTestPromotesAfterActiveSessionCompletes(t *testing.T) { server := New(Options{Store: &memoryStore{}}) firstReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":1}`)) firstRes := httptest.NewRecorder() server.Handler().ServeHTTP(firstRes, firstReq) if firstRes.Code != http.StatusAccepted { t.Fatalf("first status = %d, want 202; body=%s", firstRes.Code, firstRes.Body.String()) } var first createTestResponse if err := json.Unmarshal(firstRes.Body.Bytes(), &first); err != nil { t.Fatalf("decode first response: %v", err) } secondReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondReq) if secondRes.Code != http.StatusAccepted { t.Fatalf("second status = %d, want 202; body=%s", secondRes.Code, secondRes.Body.String()) } var second createTestResponse if err := json.Unmarshal(secondRes.Body.Bytes(), &second); err != nil { t.Fatalf("decode second response: %v", err) } if second.Status != "queued" || second.QueuePosition != 1 { t.Fatalf("second = %#v, want queued position 1", second) } queueReq := httptest.NewRequest(http.MethodGet, second.QueueURL, nil) queueRes := httptest.NewRecorder() server.Handler().ServeHTTP(queueRes, queueReq) if queueRes.Code != http.StatusOK { t.Fatalf("queue status = %d, want 200; body=%s", queueRes.Code, queueRes.Body.String()) } var queued queueStatusResponse if err := json.Unmarshal(queueRes.Body.Bytes(), &queued); err != nil { t.Fatalf("decode queue status: %v", err) } if queued.Status != "queued" || queued.QueuePosition != 1 { t.Fatalf("queue status = %#v, want queued position 1", queued) } completeReq := httptest.NewRequest(http.MethodPost, first.CompleteURL, jsonBody(`{"samples":[]}`)) completeRes := httptest.NewRecorder() server.Handler().ServeHTTP(completeRes, completeReq) if completeRes.Code != http.StatusOK { t.Fatalf("complete status = %d, want 200; body=%s", completeRes.Code, completeRes.Body.String()) } readyReq := httptest.NewRequest(http.MethodGet, second.QueueURL, nil) readyRes := httptest.NewRecorder() server.Handler().ServeHTTP(readyRes, readyReq) if readyRes.Code != http.StatusOK { t.Fatalf("ready status = %d, want 200; body=%s", readyRes.Code, readyRes.Body.String()) } var ready queueStatusResponse if err := json.Unmarshal(readyRes.Body.Bytes(), &ready); err != nil { t.Fatalf("decode ready status: %v", err) } if ready.Status != "ready" || ready.QueuePosition != 0 { t.Fatalf("ready status = %#v, want ready position 0", ready) } downloadReq := httptest.NewRequest(http.MethodGet, second.DownloadURL+"&bytes=16", nil) downloadRes := httptest.NewRecorder() server.Handler().ServeHTTP(downloadRes, downloadReq) if downloadRes.Code != http.StatusOK || downloadRes.Body.Len() != 16 { t.Fatalf("download status/body = %d/%d, want 200/16; body=%s", downloadRes.Code, downloadRes.Body.Len(), downloadRes.Body.String()) } } func TestCancelActiveTestPromotesQueuedSession(t *testing.T) { server := New(Options{Store: &memoryStore{}}) firstReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":30}`)) firstRes := httptest.NewRecorder() server.Handler().ServeHTTP(firstRes, firstReq) if firstRes.Code != http.StatusAccepted { t.Fatalf("first status = %d, want 202; body=%s", firstRes.Code, firstRes.Body.String()) } var first createTestResponse if err := json.Unmarshal(firstRes.Body.Bytes(), &first); err != nil { t.Fatalf("decode first response: %v", err) } secondReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondReq) if secondRes.Code != http.StatusAccepted { t.Fatalf("second status = %d, want 202; body=%s", secondRes.Code, secondRes.Body.String()) } var second createTestResponse if err := json.Unmarshal(secondRes.Body.Bytes(), &second); err != nil { t.Fatalf("decode second response: %v", err) } if second.Status != "queued" { t.Fatalf("second status = %q, want queued", second.Status) } cancelReq := httptest.NewRequest(http.MethodPost, first.CancelURL, nil) cancelRes := httptest.NewRecorder() server.Handler().ServeHTTP(cancelRes, cancelReq) if cancelRes.Code != http.StatusOK { t.Fatalf("cancel status = %d, want 200; body=%s", cancelRes.Code, cancelRes.Body.String()) } queueReq := httptest.NewRequest(http.MethodGet, second.QueueURL, nil) queueRes := httptest.NewRecorder() server.Handler().ServeHTTP(queueRes, queueReq) if queueRes.Code != http.StatusOK { t.Fatalf("queue status = %d, want 200; body=%s", queueRes.Code, queueRes.Body.String()) } var ready queueStatusResponse if err := json.Unmarshal(queueRes.Body.Bytes(), &ready); err != nil { t.Fatalf("decode queue status: %v", err) } if ready.Status != "ready" || ready.QueuePosition != 0 { t.Fatalf("queue status = %#v, want promoted ready session", ready) } } func TestCancelQueuedTestRemovesItAndUpdatesPositions(t *testing.T) { server := New(Options{Store: &memoryStore{}}) firstReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":30}`)) firstRes := httptest.NewRecorder() server.Handler().ServeHTTP(firstRes, firstReq) if firstRes.Code != http.StatusAccepted { t.Fatalf("first status = %d, want 202; body=%s", firstRes.Code, firstRes.Body.String()) } secondReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondReq) if secondRes.Code != http.StatusAccepted { t.Fatalf("second status = %d, want 202; body=%s", secondRes.Code, secondRes.Body.String()) } var second createTestResponse if err := json.Unmarshal(secondRes.Body.Bytes(), &second); err != nil { t.Fatalf("decode second response: %v", err) } thirdReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.net","durationSeconds":1}`)) thirdRes := httptest.NewRecorder() server.Handler().ServeHTTP(thirdRes, thirdReq) if thirdRes.Code != http.StatusAccepted { t.Fatalf("third status = %d, want 202; body=%s", thirdRes.Code, thirdRes.Body.String()) } var third createTestResponse if err := json.Unmarshal(thirdRes.Body.Bytes(), &third); err != nil { t.Fatalf("decode third response: %v", err) } if third.QueuePosition != 2 { t.Fatalf("third position = %d, want 2 before cancel", third.QueuePosition) } cancelReq := httptest.NewRequest(http.MethodPost, second.CancelURL, nil) cancelRes := httptest.NewRecorder() server.Handler().ServeHTTP(cancelRes, cancelReq) if cancelRes.Code != http.StatusOK { t.Fatalf("cancel status = %d, want 200; body=%s", cancelRes.Code, cancelRes.Body.String()) } secondQueueReq := httptest.NewRequest(http.MethodGet, second.QueueURL, nil) secondQueueRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondQueueRes, secondQueueReq) if secondQueueRes.Code != http.StatusNotFound { t.Fatalf("canceled queue status = %d, want 404; body=%s", secondQueueRes.Code, secondQueueRes.Body.String()) } thirdQueueReq := httptest.NewRequest(http.MethodGet, third.QueueURL, nil) thirdQueueRes := httptest.NewRecorder() server.Handler().ServeHTTP(thirdQueueRes, thirdQueueReq) if thirdQueueRes.Code != http.StatusOK { t.Fatalf("third queue status = %d, want 200; body=%s", thirdQueueRes.Code, thirdQueueRes.Body.String()) } var queued queueStatusResponse if err := json.Unmarshal(thirdQueueRes.Body.Bytes(), &queued); err != nil { t.Fatalf("decode third queue status: %v", err) } if queued.Status != "queued" || queued.QueuePosition != 1 { t.Fatalf("third queue status = %#v, want queued position 1 after cancel", queued) } } func TestCreateIperf3SessionStartsOneOffServerAndUsesGlobalLock(t *testing.T) { server := New(Options{Store: &memoryStore{}, Iperf3Path: fakeIperf3(t)}) req := httptest.NewRequest(http.MethodPost, "/api/iperf3/sessions", jsonBody(`{"durationSeconds":1,"protocol":"tcp"}`)) req.RemoteAddr = "203.0.113.20:54321" res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusAccepted { t.Fatalf("status = %d, want 202; body=%s", res.Code, res.Body.String()) } var payload createIperf3Response if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.ID == "" || payload.Port < 20000 || payload.CompleteURL != "/api/iperf3/sessions/"+payload.ID+"/complete" { t.Fatalf("payload = %#v, want id, random high port, and complete URL", payload) } secondReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondReq) if secondRes.Code != http.StatusAccepted { t.Fatalf("second status = %d, want 202 queued while iperf3 session is active; body=%s", secondRes.Code, secondRes.Body.String()) } var queued createTestResponse if err := json.Unmarshal(secondRes.Body.Bytes(), &queued); err != nil { t.Fatalf("decode queued response: %v", err) } if queued.Status != "queued" || queued.QueuePosition != 1 { t.Fatalf("queued response = %#v, want queued position 1", queued) } } func TestCompleteIperf3SessionPersistsRecordAndReleasesLock(t *testing.T) { store := &memoryStore{} server := New(Options{Store: store, Iperf3Path: fakeIperf3(t), ClientResolver: staticResolver}) createReq := httptest.NewRequest(http.MethodPost, "/api/iperf3/sessions", jsonBody(`{"durationSeconds":1,"protocol":"tcp","reverse":true}`)) createReq.RemoteAddr = "203.0.113.20:54321" createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createIperf3Response if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } body := `{ "startedAt":"2026-06-06T01:00:00Z", "finishedAt":"2026-06-06T01:00:01Z", "samples":[{"at":"2026-06-06T01:00:01Z","kind":"download","success":true,"bytes":1048576,"mbps":88.5}] }` completeReq := httptest.NewRequest(http.MethodPost, created.CompleteURL, jsonBody(body)) completeRes := httptest.NewRecorder() server.Handler().ServeHTTP(completeRes, completeReq) if completeRes.Code != http.StatusOK { t.Fatalf("complete status = %d, want 200; body=%s", completeRes.Code, completeRes.Body.String()) } var completed completeTestResponse if err := json.Unmarshal(completeRes.Body.Bytes(), &completed); err != nil { t.Fatalf("decode complete response: %v", err) } if completed.Summary.AvgDownloadMbps != 88.5 || !strings.Contains(completed.Summary.Target, "iperf3/tcp") { t.Fatalf("summary = %#v, want iperf3 download summary", completed.Summary) } records := store.snapshot() if len(records) != 1 || records[0].Client.IP != "203.0.113.*" || len(records[0].Samples) != 1 { t.Fatalf("records = %#v, want one persisted iperf3 record with anonymized client", records) } nextReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) nextRes := httptest.NewRecorder() server.Handler().ServeHTTP(nextRes, nextReq) if nextRes.Code != http.StatusAccepted { t.Fatalf("next status = %d, want 202 after iperf3 completion releases lock; body=%s", nextRes.Code, nextRes.Body.String()) } } func TestBrowserSessionQueuesDuringDownloadAndUploadWindows(t *testing.T) { server := New(Options{Store: &memoryStore{}}) firstReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":30}`)) firstRes := httptest.NewRecorder() server.Handler().ServeHTTP(firstRes, firstReq) if firstRes.Code != http.StatusAccepted { t.Fatalf("first status = %d, want 202; body=%s", firstRes.Code, firstRes.Body.String()) } server.mu.Lock() request := server.sessions[server.activeID] request.CreatedAt = time.Now().UTC().Add(-90 * time.Second) server.sessions[server.activeID] = request server.mu.Unlock() secondReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondReq) if secondRes.Code != http.StatusAccepted { t.Fatalf("second status = %d, want 202 queued while 30s download + 30s upload window is active; body=%s", secondRes.Code, secondRes.Body.String()) } var queued createTestResponse if err := json.Unmarshal(secondRes.Body.Bytes(), &queued); err != nil { t.Fatalf("decode queued response: %v", err) } if queued.Status != "queued" || queued.QueuePosition != 1 { t.Fatalf("queued response = %#v, want queued position 1", queued) } } func TestEventsEndpointIsRemoved(t *testing.T) { server := New(Options{Store: &memoryStore{}}) createReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":1}`)) createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createTestResponse if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } if strings.Contains(createRes.Body.String(), "eventsUrl") { t.Fatalf("create response should not advertise an events URL: %s", createRes.Body.String()) } eventsReq := httptest.NewRequest(http.MethodGet, "/api/tests/"+created.ID+"/events", nil) eventsRes := httptest.NewRecorder() server.Handler().ServeHTTP(eventsRes, eventsReq) if eventsRes.Code != http.StatusNotFound { t.Fatalf("events status = %d, want 404 now that the SSE endpoint is removed; body=%s", eventsRes.Code, eventsRes.Body.String()) } } func TestCompleteEndpointPersistsBrowserSpeedRecordAndReleasesLock(t *testing.T) { store := &memoryStore{} server := New(Options{Store: store, ClientResolver: staticResolver}) createReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":30}`)) createReq.RemoteAddr = "203.0.113.20:54321" createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createTestResponse if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } body := `{ "startedAt":"2026-06-05T08:00:00Z", "finishedAt":"2026-06-05T08:00:05Z", "samples":[ {"at":"2026-06-05T08:00:01Z","kind":"latency","success":true,"latencyMs":20}, {"at":"2026-06-05T08:00:02Z","kind":"download","success":true,"bytes":4194304,"mbps":75.5}, {"at":"2026-06-05T08:00:03Z","kind":"upload","success":true,"bytes":2097152,"mbps":42.25} ] }` completeReq := httptest.NewRequest(http.MethodPost, created.CompleteURL, jsonBody(body)) completeRes := httptest.NewRecorder() server.Handler().ServeHTTP(completeRes, completeReq) if completeRes.Code != http.StatusOK { t.Fatalf("complete status = %d, want 200; body=%s", completeRes.Code, completeRes.Body.String()) } var completed completeTestResponse if err := json.Unmarshal(completeRes.Body.Bytes(), &completed); err != nil { t.Fatalf("decode complete response: %v", err) } if completed.Summary.AvgDownloadMbps != 75.5 || completed.Summary.AvgUploadMbps != 42.25 { t.Fatalf("summary speeds = %.2f/%.2f, want 75.5/42.25", completed.Summary.AvgDownloadMbps, completed.Summary.AvgUploadMbps) } records := store.snapshot() if len(records) != 1 { t.Fatalf("records = %d, want 1", len(records)) } if records[0].Client.IP != "203.0.113.*" { t.Fatalf("record client IP = %q, want 203.0.113.*", records[0].Client.IP) } if len(records[0].Samples) != 3 { t.Fatalf("record samples = %d, want 3", len(records[0].Samples)) } nextReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.org","durationSeconds":1}`)) nextRes := httptest.NewRecorder() server.Handler().ServeHTTP(nextRes, nextReq) if nextRes.Code != http.StatusAccepted { t.Fatalf("next status = %d, want 202 after complete releases lock; body=%s", nextRes.Code, nextRes.Body.String()) } } func TestCompleteEndpointAcceptsLargeSamplePayload(t *testing.T) { store := &memoryStore{} server := New(Options{Store: store, ClientResolver: staticResolver}) createReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":30}`)) createReq.RemoteAddr = "203.0.113.20:54321" createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createTestResponse if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } completeReq := httptest.NewRequest(http.MethodPost, created.CompleteURL, largeCompleteBody(t, 30000)) completeRes := httptest.NewRecorder() server.Handler().ServeHTTP(completeRes, completeReq) if completeRes.Code != http.StatusOK { t.Fatalf("complete status = %d, want 200 for large sample payload; body=%s", completeRes.Code, completeRes.Body.String()) } records := store.snapshot() if len(records) != 1 || len(records[0].Samples) != 30000 { t.Fatalf("records = %d samples=%d, want one record with 30000 samples", len(records), len(records[0].Samples)) } } func TestCompleteEndpointCanRetryWhenPersistFails(t *testing.T) { store := &flakyStore{fail: true} server := New(Options{Store: store, ClientResolver: staticResolver}) createReq := httptest.NewRequest(http.MethodPost, "/api/tests", jsonBody(`{"target":"example.com","durationSeconds":30}`)) createReq.RemoteAddr = "203.0.113.20:54321" createRes := httptest.NewRecorder() server.Handler().ServeHTTP(createRes, createReq) if createRes.Code != http.StatusAccepted { t.Fatalf("create status = %d, want 202; body=%s", createRes.Code, createRes.Body.String()) } var created createTestResponse if err := json.Unmarshal(createRes.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } firstComplete := httptest.NewRequest(http.MethodPost, created.CompleteURL, jsonBody(`{"samples":[{"kind":"download","success":true,"bytes":1024,"mbps":10}]}`)) firstRes := httptest.NewRecorder() server.Handler().ServeHTTP(firstRes, firstComplete) if firstRes.Code != http.StatusInternalServerError { t.Fatalf("first complete status = %d, want 500; body=%s", firstRes.Code, firstRes.Body.String()) } store.fail = false secondComplete := httptest.NewRequest(http.MethodPost, created.CompleteURL, jsonBody(`{"samples":[{"kind":"download","success":true,"bytes":1024,"mbps":10}]}`)) secondRes := httptest.NewRecorder() server.Handler().ServeHTTP(secondRes, secondComplete) if secondRes.Code != http.StatusOK { t.Fatalf("retry complete status = %d, want 200 after store recovers; body=%s", secondRes.Code, secondRes.Body.String()) } if records := store.snapshot(); len(records) != 1 { t.Fatalf("records = %d, want one record after retry", len(records)) } } func TestBrowserSummaryUsesSustainedTransferRatesForMax(t *testing.T) { start := time.Date(2026, 6, 10, 8, 0, 0, 0, time.UTC) request := TestRequest{ ID: "test-1", Target: probe.Target{Address: "example.com:443"}, CreatedAt: start, } raw := completeTestRequest{ StartedAt: start, FinishedAt: start.Add(5 * time.Second), Samples: []probe.Sample{ {At: start.Add(1 * time.Second), Kind: "download", Success: true, LatencyMS: 1000, Bytes: 5_000_000, Mbps: 40}, {At: start.Add(2 * time.Second), Kind: "download", Success: true, LatencyMS: 1000, Bytes: 5_000_000, Mbps: 1000}, {At: start.Add(3 * time.Second), Kind: "download", Success: true, LatencyMS: 1000, Bytes: 5_000_000, Mbps: 40}, {At: start.Add(4 * time.Second), Kind: "download", Success: true, LatencyMS: 1000, Bytes: 5_000_000, Mbps: 40}, }, } summary := summarizeBrowserSamples(request, raw) if summary.AvgDownloadMbps != 40 { t.Fatalf("AvgDownloadMbps = %.2f, want sustained 40 Mbps", summary.AvgDownloadMbps) } if summary.MaxDownloadMbps != 40 { t.Fatalf("MaxDownloadMbps = %.2f, want sustained 40 Mbps instead of single-sample spike", summary.MaxDownloadMbps) } } func TestBrowserSummaryDoesNotTreatShortBurstsAsSustainedMax(t *testing.T) { start := time.Date(2026, 6, 10, 8, 0, 0, 0, time.UTC) var samples []probe.Sample elapsed := time.Duration(0) for i := 0; i < 10; i++ { latency := 50 * time.Millisecond mbps := 671.09 if i%2 == 1 { latency = 1750 * time.Millisecond mbps = 19.17 } elapsed += latency samples = append(samples, probe.Sample{ At: start.Add(elapsed), Kind: "download", Success: true, LatencyMS: float64(latency.Milliseconds()), Bytes: 4 * 1024 * 1024, Mbps: mbps, }) } request := TestRequest{ ID: "test-1", Target: probe.Target{Address: "example.com:443"}, CreatedAt: start, } raw := completeTestRequest{ StartedAt: start, FinishedAt: start.Add(elapsed), Samples: samples, } summary := summarizeBrowserSamples(request, raw) if summary.MaxDownloadMbps > 60 { t.Fatalf("MaxDownloadMbps = %.2f, want sustained max below short burst rates", summary.MaxDownloadMbps) } if summary.AvgDownloadMbps < 35 || summary.AvgDownloadMbps > 39 { t.Fatalf("AvgDownloadMbps = %.2f, want long-run average around 37 Mbps", summary.AvgDownloadMbps) } } func TestSSEUnknownSessionReturnsNotFound(t *testing.T) { server := New(Options{Store: &memoryStore{}}) req := httptest.NewRequest(http.MethodGet, "/api/tests/missing/events", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", res.Code) } } func TestHandlerServesStaticFilesFromFS(t *testing.T) { staticFS := http.FS(fstest.MapFS{ "index.html": &fstest.MapFile{Data: []byte("dashboard")}, }) server := New(Options{Store: &memoryStore{}, StaticFS: staticFS}) req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } if !strings.Contains(res.Body.String(), "dashboard") { t.Fatalf("body = %q, want dashboard content", res.Body.String()) } } func TestDownloadsEndpointServesPackagedLinuxBinary(t *testing.T) { downloadsDir := t.TempDir() packagePath := filepath.Join(downloadsDir, "netstable-linux-amd64.tar.gz") if err := os.WriteFile(packagePath, []byte("fake-package"), 0644); err != nil { t.Fatalf("write fake package: %v", err) } server := New(Options{Store: &memoryStore{}, DownloadsDir: downloadsDir}) req := httptest.NewRequest(http.MethodGet, "/downloads/netstable-linux-amd64.tar.gz", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", res.Code, res.Body.String()) } if res.Body.String() != "fake-package" { t.Fatalf("body = %q, want fake package", res.Body.String()) } if !strings.Contains(res.Header().Get("Content-Disposition"), "netstable-linux-amd64.tar.gz") { t.Fatalf("Content-Disposition = %q, want filename", res.Header().Get("Content-Disposition")) } } func TestDownloadsEndpointRejectsUnknownBinaryName(t *testing.T) { server := New(Options{Store: &memoryStore{}, DownloadsDir: t.TempDir()}) req := httptest.NewRequest(http.MethodGet, "/downloads/netstable-linux-riscv64", nil) res := httptest.NewRecorder() server.Handler().ServeHTTP(res, req) if res.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", res.Code) } } func TestSubFSHelperReturnsStaticRoot(t *testing.T) { staticFS := fstest.MapFS{ "static/index.html": &fstest.MapFile{Data: []byte("embedded")}, } root, err := fs.Sub(staticFS, "static") if err != nil { t.Fatalf("fs.Sub: %v", err) } if _, err := root.Open("index.html"); err != nil { t.Fatalf("open index from sub fs: %v", err) } } func jsonBody(body string) *bytes.Buffer { return bytes.NewBufferString(body) } func largeCompleteBody(t *testing.T, sampleCount int) *bytes.Buffer { t.Helper() var buffer bytes.Buffer buffer.WriteString(`{"startedAt":"2026-06-05T08:00:00Z","finishedAt":"2026-06-05T08:00:30Z","samples":[`) for i := 0; i < sampleCount; i++ { if i > 0 { buffer.WriteByte(',') } kind := "download" if i%2 == 1 { kind = "upload" } if err := json.NewEncoder(&buffer).Encode(probe.Sample{ At: time.Date(2026, 6, 5, 8, 0, 0, 0, time.UTC).Add(time.Duration(i) * time.Millisecond), Kind: kind, Success: true, Bytes: 4 * 1024 * 1024, Mbps: 42.5, LatencyMS: 150, }); err != nil { t.Fatalf("encode sample: %v", err) } data := buffer.Bytes() if len(data) > 0 && data[len(data)-1] == '\n' { buffer.Truncate(len(data) - 1) } } buffer.WriteString(`]}`) return &buffer } type memoryStore struct { mu sync.Mutex records []recordstore.Record } func (s *memoryStore) Append(ctx context.Context, record recordstore.Record) error { s.mu.Lock() defer s.mu.Unlock() s.records = append([]recordstore.Record{record}, s.records...) return nil } func (s *memoryStore) List(ctx context.Context, limit int) ([]recordstore.Record, error) { s.mu.Lock() defer s.mu.Unlock() if limit < 1 || limit > len(s.records) { limit = len(s.records) } out := make([]recordstore.Record, limit) copy(out, s.records[:limit]) return out, nil } func (s *memoryStore) snapshot() []recordstore.Record { s.mu.Lock() defer s.mu.Unlock() out := make([]recordstore.Record, len(s.records)) copy(out, s.records) return out } type flakyStore struct { memoryStore mu sync.Mutex fail bool } func (s *flakyStore) Append(ctx context.Context, record recordstore.Record) error { s.mu.Lock() fail := s.fail s.mu.Unlock() if fail { return errors.New("store unavailable") } return s.memoryStore.Append(ctx, record) } func staticResolver(ctx context.Context, ip string) (recordstore.ClientInfo, error) { return recordstore.ClientInfo{ IP: ip, ISP: "Example Mobile", Region: "Beijing", Country: "China", City: "Beijing", ASN: "AS64512", Source: "test", }, nil } func fakeIperf3(t *testing.T) string { t.Helper() path := filepath.Join(t.TempDir(), "iperf3") script := `#!/bin/sh for arg in "$@"; do if [ "$arg" = "-s" ]; then sleep 10 exit 0 fi done cat <<'JSON' {"end":{"sum_sent":{"bytes":1048576,"bits_per_second":88000000},"sum_received":{"bytes":1048576,"bits_per_second":87000000}}} JSON ` if err := os.WriteFile(path, []byte(script), 0755); err != nil { t.Fatalf("write fake iperf3: %v", err) } return path }