diff --git a/internal/gc/gc_integration_test.go b/internal/gc/gc_integration_test.go index 991facc..fa5b413 100644 --- a/internal/gc/gc_integration_test.go +++ b/internal/gc/gc_integration_test.go @@ -94,3 +94,21 @@ func TestSweepNoOrphans(t *testing.T) { // A sweep with nothing to collect should be a clean no-op. New(testDB, testStore, time.Hour).sweep(context.Background()) } + +func TestRunStopsOnContextCancel(t *testing.T) { + if testDB == nil { + t.Skip("Docker unavailable") + } + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + New(testDB, testStore, time.Hour).Run(ctx) + close(done) + }() + cancel() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("Run did not return after context cancel") + } +} diff --git a/internal/provider/rpm/rpm_meta_test.go b/internal/provider/rpm/rpm_meta_test.go index 2233839..64ed91e 100644 --- a/internal/provider/rpm/rpm_meta_test.go +++ b/internal/provider/rpm/rpm_meta_test.go @@ -105,6 +105,27 @@ func TestRPMAfterUpload(t *testing.T) { } } +type errBlobReader struct{} + +func (errBlobReader) Download(_ context.Context, _ string) (io.ReadCloser, int64, error) { + return nil, 0, io.ErrUnexpectedEOF +} + +func TestRPMAfterUploadErrors(t *testing.T) { + // Download failure: no metadata inserted, no panic. + store := &fakeMetaStore{} + (&Provider{}).AfterUpload(context.Background(), "r", "p", "sha256:x", errBlobReader{}, store) + if store.inserted != nil { + t.Error("no metadata should be inserted on download error") + } + // Parse failure: garbage bytes are not a valid RPM. + store2 := &fakeMetaStore{} + (&Provider{}).AfterUpload(context.Background(), "r", "p", "sha256:x", fakeBlobReader{data: []byte("not an rpm")}, store2) + if store2.inserted != nil { + t.Error("no metadata should be inserted on parse error") + } +} + func TestRPMServeRepodata(t *testing.T) { p := &Provider{} reader := fakeRPMReader{metas: []provider.RPMMetadata{{ diff --git a/internal/proxy/engine_test.go b/internal/proxy/engine_test.go index bd30be2..3562c24 100644 --- a/internal/proxy/engine_test.go +++ b/internal/proxy/engine_test.go @@ -293,6 +293,21 @@ func TestBearerTokenFlow(t *testing.T) { } } +func TestFetchUpstreamError(t *testing.T) { + requireStack(t) + r := seed(t, genericRemote("eng-404")) + // Upstream 404 (no cached copy, stale-on-error can't help) -> ProxyError. + _, err := testEngine.Fetch(context.Background(), r, "missing", prov(t, models.PackageGeneric)) + var pe *ProxyError + if err == nil || !asProxyError(err, &pe) || pe.Status != http.StatusNotFound { + t.Errorf("expected 404 ProxyError, got %v", err) + } + // HEAD of a missing upstream path also errors. + if _, err := testEngine.Head(context.Background(), r, "missing", prov(t, models.PackageGeneric)); err == nil { + t.Error("expected head error for missing path") + } +} + func TestBearerTokenParsing(t *testing.T) { // Non-Bearer challenges and missing realms are rejected. if _, _, err := fetchBearerToken(context.Background(), "Basic realm=x", models.Remote{}); err == nil { diff --git a/internal/server/server_test.go b/internal/server/server_test.go index bf3eef2..5288cdd 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -227,12 +227,28 @@ func TestServerVirtualMerge(t *testing.T) { func TestServerProbe(t *testing.T) { requireStack(t) - body := fmt.Sprintf(`{"package_type":"generic","base_url":%q,"path":"data/file.bin"}`, upstream.URL) - resp, _ := req(t, "POST", "/api/v2/probe", body) - // Probe should reach the mock upstream and report reachable (200) or a - // structured result; either way not a server error. - if resp.StatusCode >= 500 { - t.Errorf("probe server error: %d", resp.StatusCode) + create := fmt.Sprintf(`{"name":"srv-probe","package_type":"generic","repo_type":"remote","base_url":%q,"stale_on_error":true}`, upstream.URL) + req(t, "POST", "/api/v2/remotes", create) + defer req(t, "DELETE", "/api/v2/remotes/srv-probe", "") + + // Reachable path -> status 200 in the probe body. + if resp, b := req(t, "POST", "/api/v2/probe", `{"remote":"srv-probe","path":"data/file.bin"}`); resp.StatusCode != 200 || !strings.Contains(string(b), `"status":200`) { + t.Errorf("probe reachable: %d %s", resp.StatusCode, b) + } + // Missing upstream path -> upstream error reported (502) in the body. + if resp, b := req(t, "POST", "/api/v2/probe", `{"remote":"srv-probe","path":"missing"}`); resp.StatusCode != 200 || !strings.Contains(string(b), `"status":502`) { + t.Errorf("probe missing: %d %s", resp.StatusCode, b) + } + // Unknown remote -> 404 in the body. + if resp, b := req(t, "POST", "/api/v2/probe", `{"remote":"nope","path":"x"}`); resp.StatusCode != 200 || !strings.Contains(string(b), `"status":404`) { + t.Errorf("probe unknown: %d %s", resp.StatusCode, b) + } + // Bad requests. + if resp, _ := req(t, "POST", "/api/v2/probe", `{}`); resp.StatusCode != 400 { + t.Errorf("probe missing fields: %d", resp.StatusCode) + } + if resp, _ := req(t, "POST", "/api/v2/probe", `not json`); resp.StatusCode != 400 { + t.Errorf("probe invalid json: %d", resp.StatusCode) } } @@ -386,11 +402,31 @@ func TestServerLocalRemoveAndMissing(t *testing.T) { } } -func TestServerProbeUnreachable(t *testing.T) { +func TestServerLocalUploadErrors(t *testing.T) { requireStack(t) - resp, _ := req(t, "POST", "/api/v2/probe", `{"package_type":"generic","base_url":"http://127.0.0.1:1","path":"x"}`) - if resp.StatusCode >= 500 { - t.Errorf("probe of unreachable upstream should not 500: %d", resp.StatusCode) + // Uploading to a remote-type repo is rejected. + create := fmt.Sprintf(`{"name":"srv-uerr","package_type":"generic","repo_type":"remote","base_url":%q,"stale_on_error":true}`, upstream.URL) + req(t, "POST", "/api/v2/remotes", create) + defer req(t, "DELETE", "/api/v2/remotes/srv-uerr", "") + if resp, _ := put(t, "/api/v2/remotes/srv-uerr/files/x.bin", []byte("x")); resp.StatusCode != 400 { + t.Errorf("upload to remote repo should be 400, got %d", resp.StatusCode) + } + + // Duplicate generic upload is a conflict. + req(t, "POST", "/api/v2/remotes", `{"name":"srv-dup","package_type":"generic","repo_type":"local"}`) + defer req(t, "DELETE", "/api/v2/remotes/srv-dup", "") + put(t, "/api/v2/remotes/srv-dup/files/dup.bin", []byte("one")) + if resp, _ := put(t, "/api/v2/remotes/srv-dup/files/dup.bin", []byte("two")); resp.StatusCode != 409 { + t.Errorf("duplicate upload should be 409, got %d", resp.StatusCode) + } + + // Download of a missing local file is 404. + if resp, _ := req(t, "GET", "/api/v1/local/srv-dup/does/not/exist", ""); resp.StatusCode != 404 { + t.Errorf("missing local download should be 404, got %d", resp.StatusCode) + } + // Unknown virtual is 404. + if resp, _ := req(t, "GET", "/api/v1/virtual/nope/index.yaml", ""); resp.StatusCode != 404 { + t.Errorf("unknown virtual should be 404, got %d", resp.StatusCode) } }