From 6029f19b868b537066242536f8d7108e3b8b354d Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Fri, 3 Jul 2026 13:35:40 +1000 Subject: [PATCH] test: proxy HTTP error branches (403/502) + revalidation upstream error --- internal/proxy/engine_test.go | 28 ++++++++++++++++++++++++++++ internal/server/server_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/internal/proxy/engine_test.go b/internal/proxy/engine_test.go index d4a38d4..34f541c 100644 --- a/internal/proxy/engine_test.go +++ b/internal/proxy/engine_test.go @@ -361,6 +361,34 @@ func TestWaitForStoreCoalesces(t *testing.T) { } } +func TestRevalidationUpstreamError(t *testing.T) { + requireStack(t) + ctx := context.Background() + r := seed(t, models.Remote{Name: "eng-reval-err", PackageType: models.PackageNPM, RepoType: models.RepoTypeRemote, BaseURL: upstream.URL, CheckMutable: true, MutableTTL: 3600, StaleOnError: true}) + p := prov(t, models.PackageNPM) + + res, err := testEngine.Fetch(ctx, r, "pkg", p) + if err != nil { + t.Fatalf("initial fetch: %v", err) + } + res.Reader.Close() + + // Expire freshness but keep the ETag, then break the upstream: the + // conditional HEAD (checkUpstream) errors, and stale-on-error serves the + // stored index. + testCache.SetTTL(ctx, "eng-reval-err", "pkg", time.Millisecond) + time.Sleep(10 * time.Millisecond) + r.BaseURL = "http://127.0.0.1:1" + res, err = testEngine.Fetch(ctx, r, "pkg", p) + if err != nil { + t.Fatalf("expected stale serve on revalidation error, got %v", err) + } + if res.Source != "cache" { + t.Errorf("expected stale cache source, got %s", res.Source) + } + res.Reader.Close() +} + func TestUpstreamErrorUnwrap(t *testing.T) { base := context.DeadlineExceeded ue := &UpstreamError{Err: base} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index d6a0a44..09ae339 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -504,6 +504,32 @@ func TestServerVirtualLocalPyPIMerge(t *testing.T) { } } +func TestServerProxyErrors(t *testing.T) { + requireStack(t) + // Blocklisted path -> 403 propagated through handleProxy. + block := fmt.Sprintf(`{"name":"srv-block","package_type":"generic","repo_type":"remote","base_url":%q,"blocklist":["\\.secret$"],"stale_on_error":true}`, upstream.URL) + req(t, "POST", "/api/v2/remotes", block) + defer req(t, "DELETE", "/api/v2/remotes/srv-block", "") + if resp, _ := req(t, "GET", "/api/v1/remote/srv-block/x.secret", ""); resp.StatusCode != 403 { + t.Errorf("blocklisted GET = %d, want 403", resp.StatusCode) + } + rq, _ := http.NewRequest("HEAD", testTS.URL+"/v2/srv-block/x.secret", nil) + if resp, err := http.DefaultClient.Do(rq); err == nil { + resp.Body.Close() + if resp.StatusCode != 403 { + t.Errorf("blocklisted HEAD = %d, want 403", resp.StatusCode) + } + } + + // Unreachable upstream, no stale copy -> 502 bad gateway. + dead := `{"name":"srv-dead","package_type":"generic","repo_type":"remote","base_url":"http://127.0.0.1:1","stale_on_error":false}` + req(t, "POST", "/api/v2/remotes", dead) + defer req(t, "DELETE", "/api/v2/remotes/srv-dead", "") + if resp, _ := req(t, "GET", "/api/v1/remote/srv-dead/x", ""); resp.StatusCode != 502 { + t.Errorf("dead upstream GET = %d, want 502", resp.StatusCode) + } +} + func TestServerNotFound(t *testing.T) { requireStack(t) if resp, _ := req(t, "GET", "/api/v2/remotes/does-not-exist", ""); resp.StatusCode != 404 {