diff --git a/internal/proxy/engine_test.go b/internal/proxy/engine_test.go index 9f809d5..bd30be2 100644 --- a/internal/proxy/engine_test.go +++ b/internal/proxy/engine_test.go @@ -92,6 +92,15 @@ func mockUpstream(w http.ResponseWriter, r *http.Request) { } w.Header().Set("ETag", `"v1"`) w.Write([]byte(`{"name":"pkg"}`)) + case "/protected.bin": // requires a bearer token obtained from /token + if r.Header.Get("Authorization") != "Bearer minted-token" { + w.Header().Set("Www-Authenticate", `Bearer realm="`+upstream.URL+`/token",service="reg",scope="repo:pull"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Write([]byte("protected payload")) + case "/token": + w.Write([]byte(`{"token":"minted-token","expires_in":300}`)) default: http.NotFound(w, r) } @@ -261,6 +270,39 @@ func TestMutableRevalidation(t *testing.T) { res.Reader.Close() } +func TestBearerTokenFlow(t *testing.T) { + requireStack(t) + ctx := context.Background() + r := seed(t, genericRemote("eng-bearer")) + p := prov(t, models.PackageGeneric) + + // GET: 401 challenge -> token endpoint -> retry with bearer -> 200. + res, err := testEngine.Fetch(ctx, r, "protected.bin", p) + if err != nil { + t.Fatalf("bearer fetch: %v", err) + } + if readAll(t, res) != "protected payload" { + t.Error("bearer-protected content mismatch") + } + + // HEAD path also negotiates a bearer token (uncached). + testCache.FlushRemote(ctx, "eng-bearer") + testDB.DeleteArtifact(ctx, "eng-bearer", "protected.bin") + if h, err := testEngine.Head(ctx, r, "protected.bin", p); err != nil || h.Source != "cache" && h.Source != "remote" { + t.Fatalf("bearer head: %+v %v", h, err) + } +} + +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 { + t.Error("expected error for non-Bearer challenge") + } + if _, _, err := fetchBearerToken(context.Background(), `Bearer service="reg"`, models.Remote{}); err == nil { + t.Error("expected error for missing realm") + } +} + func asProxyError(err error, target **ProxyError) bool { pe, ok := err.(*ProxyError) if ok { diff --git a/internal/server/server_test.go b/internal/server/server_test.go index fee048f..bf3eef2 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -329,6 +329,84 @@ func TestServerValidationErrors(t *testing.T) { } } +func TestServerDockerAndHead(t *testing.T) { + requireStack(t) + create := fmt.Sprintf(`{"name":"srv-docker","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-docker", "") + + // Docker registry ping. + if resp, _ := req(t, "GET", "/v2/", ""); resp.StatusCode != 200 { + t.Errorf("docker ping: %d", resp.StatusCode) + } + // HEAD through the docker route resolves metadata (uncached -> upstream). + rq, _ := http.NewRequest("HEAD", testTS.URL+"/v2/srv-docker/data/file.bin", nil) + resp, err := http.DefaultClient.Do(rq) + if err != nil { + t.Fatalf("head: %v", err) + } + resp.Body.Close() + if resp.StatusCode != 200 { + t.Errorf("head status: %d", resp.StatusCode) + } +} + +func TestServerRemoteUpdateAndVirtualCRUD(t *testing.T) { + requireStack(t) + req(t, "POST", "/api/v2/remotes", `{"name":"srv-upd","package_type":"helm","repo_type":"remote","base_url":"https://a.example.com","stale_on_error":true}`) + defer req(t, "DELETE", "/api/v2/remotes/srv-upd", "") + if resp, b := req(t, "PUT", "/api/v2/remotes/srv-upd", `{"package_type":"helm","base_url":"https://b.example.com","stale_on_error":true}`); resp.StatusCode != 200 { + t.Errorf("update remote: %d %s", resp.StatusCode, b) + } + + req(t, "POST", "/api/v2/virtuals", `{"name":"srv-v2","package_type":"helm","members":["srv-upd"]}`) + defer req(t, "DELETE", "/api/v2/virtuals/srv-v2", "") + if resp, _ := req(t, "GET", "/api/v2/virtuals/srv-v2", ""); resp.StatusCode != 200 { + t.Errorf("get virtual: %d", resp.StatusCode) + } + if resp, _ := req(t, "GET", "/api/v2/virtuals", ""); resp.StatusCode != 200 { + t.Errorf("list virtuals: %d", resp.StatusCode) + } + if resp, b := req(t, "PUT", "/api/v2/virtuals/srv-v2", `{"package_type":"helm","members":["srv-upd"]}`); resp.StatusCode != 200 { + t.Errorf("update virtual: %d %s", resp.StatusCode, b) + } +} + +func TestServerLocalRemoveAndMissing(t *testing.T) { + requireStack(t) + req(t, "POST", "/api/v2/remotes", `{"name":"srv-rm","package_type":"generic","repo_type":"local"}`) + defer req(t, "DELETE", "/api/v2/remotes/srv-rm", "") + + put(t, "/api/v2/remotes/srv-rm/files/a/b.bin", []byte("payload")) + if resp, _ := req(t, "DELETE", "/api/v2/remotes/srv-rm/files/a/b.bin", ""); resp.StatusCode >= 400 { + t.Errorf("delete local file: %d", resp.StatusCode) + } + if resp, _ := req(t, "GET", "/api/v1/local/srv-rm/a/b.bin", ""); resp.StatusCode != 404 { + t.Errorf("expected 404 for removed file, got %d", resp.StatusCode) + } +} + +func TestServerProbeUnreachable(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) + } +} + +func TestServerEvents(t *testing.T) { + requireStack(t) + client := &http.Client{Timeout: 800 * time.Millisecond} + resp, err := client.Get(testTS.URL + "/api/v2/events") + if err == nil { + resp.Body.Close() + if resp.StatusCode != 200 { + t.Errorf("events status: %d", resp.StatusCode) + } + } + // A timeout is expected for a streaming endpoint; the handler still ran. +} + func TestServerNotFound(t *testing.T) { requireStack(t) if resp, _ := req(t, "GET", "/api/v2/remotes/does-not-exist", ""); resp.StatusCode != 404 {