From 691e6ff308241f3210d66b1cb0c98ea5065b9341 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 15:31:31 +0300 Subject: [PATCH 1/2] test(web4): authenticate TestSetTagsDashboardAPI against admin-gated /api/stats The security batch moved the rich /api/stats payload behind requireAdminToken in rendezvous. zz_dashboard_test.go was updated for this (it passes ?admin_token=), but zz_tags_test.go was not, so TestSetTagsDashboardAPI now receives a 401 with the body "admin token not configured" and fails deterministically with `decode JSON: invalid character 'a' looking for beginning of value`. Configure an admin token on the test registry and pass it on the /api/stats request, matching the existing pattern in zz_dashboard_test.go. Also assert the 200 so a future auth regression fails with a clear status mismatch instead of a JSON decode error. Test-only change; no product code touched. Note: this did not show up in PR CI because requireRealNetwork skips the test on GitHub-hosted runners. It fails locally and on the self-hosted nightly runner (PILOT_REAL_NETWORK=1). Co-Authored-By: Claude Opus 5 --- tests/zz_tags_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/zz_tags_test.go b/tests/zz_tags_test.go index d8c062d5..5258e278 100644 --- a/tests/zz_tags_test.go +++ b/tests/zz_tags_test.go @@ -243,6 +243,9 @@ func TestSetTagsDashboardAPI(t *testing.T) { t.Parallel() r := registry.New("127.0.0.1:9001") + // /api/stats is admin-gated (rich payload sits behind requireAdminToken); + // configure a token so the operator view is reachable from this test. + r.SetAdminToken(TestAdminToken) go r.ListenAndServe("127.0.0.1:0") <-r.Ready() defer r.Close() @@ -280,8 +283,9 @@ func TestSetTagsDashboardAPI(t *testing.T) { var client http.Client client.Timeout = 2 * time.Second var httpResp *http.Response + statsURL := fmt.Sprintf("http://%s/api/stats?admin_token=%s", dashAddr, TestAdminToken) for i := 0; i < 20; i++ { - httpResp, err = client.Get(fmt.Sprintf("http://%s/api/stats", dashAddr)) + httpResp, err = client.Get(statsURL) if err == nil { break } @@ -292,6 +296,10 @@ func TestSetTagsDashboardAPI(t *testing.T) { } defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + t.Fatalf("GET /api/stats = %d, want 200", httpResp.StatusCode) + } + var stats registry.DashboardStats body, _ := io.ReadAll(httpResp.Body) if err := json.Unmarshal(body, &stats); err != nil { From 7fd358556635c8862f86cd00a93b56625449deb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 15:48:19 +0300 Subject: [PATCH 2/2] test(web4): assert tag privacy against /api/public-stats, not the gated endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestSetTagsDashboardNoHostname and TestSetTagsDashboardNoIPLeak assert that the response body does NOT contain a hostname, 127.0.0.1, real_addr or public_key. Since /api/stats moved behind requireAdminToken these tests receive a 401 body, which trivially satisfies every assertion — they pass while checking nothing. Point them at /api/public-stats, the curated anonymous payload that is actually the surface this privacy property is about, and assert 200 plus a non-empty body so they cannot silently go vacuous again. Test-only change. Co-Authored-By: Claude Opus 5 --- tests/zz_tags_test.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/zz_tags_test.go b/tests/zz_tags_test.go index 5258e278..8523f1b1 100644 --- a/tests/zz_tags_test.go +++ b/tests/zz_tags_test.go @@ -334,8 +334,11 @@ func TestSetTagsDashboardNoHostname(t *testing.T) { var client http.Client client.Timeout = 2 * time.Second var httpResp *http.Response + // The anonymous surface is /api/public-stats; /api/stats is admin-gated + // and would answer 401, which would satisfy the assertion below without + // ever inspecting a real payload. for i := 0; i < 20; i++ { - httpResp, err = client.Get(fmt.Sprintf("http://%s/api/stats", dashAddr)) + httpResp, err = client.Get(fmt.Sprintf("http://%s/api/public-stats", dashAddr)) if err == nil { break } @@ -346,8 +349,15 @@ func TestSetTagsDashboardNoHostname(t *testing.T) { } defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + t.Fatalf("GET /api/public-stats = %d, want 200", httpResp.StatusCode) + } + body, _ := io.ReadAll(httpResp.Body) bodyStr := string(body) + if len(bodyStr) == 0 { + t.Fatal("empty /api/public-stats body — assertion would pass vacuously") + } // Dashboard JSON should NOT contain hostname field if strings.Contains(bodyStr, "\"hostname\"") { @@ -377,8 +387,9 @@ func TestSetTagsDashboardNoIPLeak(t *testing.T) { var client http.Client client.Timeout = 2 * time.Second var httpResp *http.Response + // Anonymous surface — see the note in TestSetTagsDashboardNoHostname. for i := 0; i < 20; i++ { - httpResp, err = client.Get(fmt.Sprintf("http://%s/api/stats", dashAddr)) + httpResp, err = client.Get(fmt.Sprintf("http://%s/api/public-stats", dashAddr)) if err == nil { break } @@ -389,8 +400,15 @@ func TestSetTagsDashboardNoIPLeak(t *testing.T) { } defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + t.Fatalf("GET /api/public-stats = %d, want 200", httpResp.StatusCode) + } + body, _ := io.ReadAll(httpResp.Body) bodyStr := string(body) + if len(bodyStr) == 0 { + t.Fatal("empty /api/public-stats body — assertions would pass vacuously") + } if strings.Contains(bodyStr, "127.0.0.1") { t.Fatal("API response leaks 127.0.0.1")