package handler
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/git-pkgs/proxy/internal/cooldown"
)
func nugetTestProxy() *Proxy {
return &Proxy{
Logger: slog.Default(),
HTTPClient: http.DefaultClient,
}
}
func TestNuGetRewriteServiceIndex(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: nugetUpstream,
proxyURL: "http://localhost:8080",
}
input := `{
"version": "3.0.0",
"resources": [
{
"@id": "https://api.nuget.org/v3-flatcontainer/",
"@type": "PackageBaseAddress/3.0.0"
},
{
"@id": "https://api.nuget.org/v3/registration5-gz-semver2/",
"@type": "RegistrationsBaseUrl/3.6.0"
},
{
"@id": "https://azuresearch-usnc.nuget.org/query",
"@type": "SearchQueryService/3.5.0"
},
{
"@id": "https://azuresearch-usnc.nuget.org/autocomplete",
"@type": "SearchAutocompleteService/3.5.0"
},
{
"@id": "https://example.com/other-service",
"@type": "SomeOtherService/1.0.0"
}
]
}`
output, err := h.rewriteServiceIndex([]byte(input))
if err != nil {
t.Fatalf("rewriteServiceIndex failed: %v", err)
}
var result map[string]any
if err := json.Unmarshal(output, &result); err != nil {
t.Fatalf("failed to parse output: %v", err)
}
resources := result["resources"].([]any)
if len(resources) != 5 {
t.Fatalf("expected 5 resources, got %d", len(resources))
}
expectations := map[string]string{
"PackageBaseAddress/3.0.0": "http://localhost:8080/nuget/v3-flatcontainer/",
"RegistrationsBaseUrl/3.6.0": "http://localhost:8080/nuget/v3/registration5-gz-semver2/",
"SearchQueryService/3.5.0": "http://localhost:8080/nuget/query",
"SearchAutocompleteService/3.5.0": "http://localhost:8080/nuget/autocomplete",
"SomeOtherService/1.0.0": "https://example.com/other-service",
}
for _, res := range resources {
rmap := res.(map[string]any)
rtype := rmap["@type"].(string)
id := rmap["@id"].(string)
expected, ok := expectations[rtype]
if !ok {
t.Errorf("unexpected resource type: %s", rtype)
continue
}
if id != expected {
t.Errorf("resource %s: @id = %q, want %q", rtype, id, expected)
}
}
}
func TestNuGetShouldRewriteService(t *testing.T) {
h := &NuGetHandler{}
rewriteTypes := []string{
"PackageBaseAddress/3.0.0",
"RegistrationsBaseUrl/3.6.0",
"RegistrationsBaseUrl/Versioned",
"SearchQueryService",
"SearchQueryService/3.0.0-rc",
"SearchQueryService/3.5.0",
"SearchAutocompleteService",
"SearchAutocompleteService/3.5.0",
}
for _, stype := range rewriteTypes {
if !h.shouldRewriteService(stype) {
t.Errorf("shouldRewriteService(%q) = false, want true", stype)
}
}
noRewriteTypes := []string{
"SomeOtherService/1.0.0",
"PackagePublish/2.0.0",
"",
"SearchQueryService/99.0.0",
}
for _, stype := range noRewriteTypes {
if h.shouldRewriteService(stype) {
t.Errorf("shouldRewriteService(%q) = true, want false", stype)
}
}
}
func TestNuGetRewriteURL(t *testing.T) {
h := &NuGetHandler{
proxyURL: "http://localhost:8080",
}
tests := []struct {
input string
want string
}{
{
"https://api.nuget.org/v3-flatcontainer/",
"http://localhost:8080/nuget/v3-flatcontainer/",
},
{
"https://api.nuget.org/v3/registration5-gz-semver2/",
"http://localhost:8080/nuget/v3/registration5-gz-semver2/",
},
{
"https://azuresearch-usnc.nuget.org/query",
"http://localhost:8080/nuget/query",
},
{
"https://azuresearch-usnc.nuget.org/autocomplete",
"http://localhost:8080/nuget/autocomplete",
},
{
"https://example.com/unknown",
"https://example.com/unknown",
},
}
for _, tt := range tests {
got := h.rewriteNuGetURL(tt.input)
if got != tt.want {
t.Errorf("rewriteNuGetURL(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestNuGetHandleServiceIndex(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v3/index.json" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"version": "3.0.0",
"resources": [
{
"@id": "https://api.nuget.org/v3-flatcontainer/",
"@type": "PackageBaseAddress/3.0.0"
}
]
}`))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3/index.json", nil)
w := httptest.NewRecorder()
h.handleServiceIndex(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want %q", ct, "application/json")
}
var result map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
resources := result["resources"].([]any)
r0 := resources[0].(map[string]any)
if r0["@id"] != "http://proxy.local/nuget/v3-flatcontainer/" {
t.Errorf("resource @id = %q, want rewritten URL", r0["@id"])
}
}
func TestNuGetHandleServiceIndexUpstreamError(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("internal error"))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3/index.json", nil)
w := httptest.NewRecorder()
h.handleServiceIndex(w, req)
// With metadata caching, upstream 500 is reported as 502 (bad gateway)
if w.Code != http.StatusBadGateway {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadGateway)
}
}
func TestNuGetHandleServiceIndexUpstreamUnreachable(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: "http://127.0.0.1:1", // unreachable
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3/index.json", nil)
w := httptest.NewRecorder()
h.handleServiceIndex(w, req)
if w.Code != http.StatusBadGateway {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadGateway)
}
}
func TestNuGetHandleServiceIndexInvalidJSON(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte("not valid json"))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3/index.json", nil)
w := httptest.NewRecorder()
h.handleServiceIndex(w, req)
// When rewrite fails, the handler falls back to proxying the original body
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d (should fall back to original)", w.Code, http.StatusOK)
}
body := w.Body.String()
if body != "not valid json" {
t.Errorf("body = %q, want original body passed through", body)
}
}
func TestNuGetHandleDownloadEmptyParams(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: "http://localhost:1",
proxyURL: "http://proxy.local",
}
// Missing path values
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer///", nil)
req.SetPathValue("id", "")
req.SetPathValue("version", "")
req.SetPathValue("filename", "")
w := httptest.NewRecorder()
h.handleDownload(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestNuGetHandleDownloadNonNupkg(t *testing.T) {
// Non-.nupkg files should be proxied upstream
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("test"))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", nil)
req.SetPathValue("id", "newtonsoft.json")
req.SetPathValue("version", "13.0.1")
req.SetPathValue("filename", "newtonsoft.json.nuspec")
w := httptest.NewRecorder()
h.handleDownload(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if body != "test" {
t.Errorf("body = %q, want nuspec content", body)
}
}
func TestNuGetProxyUpstream(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v3-flatcontainer/newtonsoft.json/index.json" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"versions":["13.0.1","13.0.2"]}`))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/newtonsoft.json/index.json", nil)
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !strings.Contains(body, "13.0.1") {
t.Errorf("response body does not contain expected version: %s", body)
}
}
func TestNuGetProxyUpstreamNotFound(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/nonexistent/index.json", nil)
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
}
func TestNuGetProxyUpstreamBadUpstream(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: "http://127.0.0.1:1",
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test/index.json", nil)
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
if w.Code != http.StatusBadGateway {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadGateway)
}
}
func TestNuGetProxyUpstreamCopiesHeaders(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Custom", "value")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test/index.json", nil)
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
if w.Header().Get("X-Custom") != "value" {
t.Errorf("X-Custom = %q, want %q", w.Header().Get("X-Custom"), "value")
}
}
func TestNuGetProxyUpstreamForwardsAcceptEncoding(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ae := r.Header.Get("Accept-Encoding")
if ae != "gzip" {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("expected Accept-Encoding: gzip"))
return
}
w.WriteHeader(http.StatusOK)
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test/index.json", nil)
req.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestNuGetBuildUpstreamURL(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
}
tests := []struct {
path string
query string
want string
}{
{
"/v3-flatcontainer/newtonsoft.json/index.json",
"",
"https://api.nuget.org/v3-flatcontainer/newtonsoft.json/index.json",
},
{
"/v3/registration5-gz-semver2/newtonsoft.json/index.json",
"",
"https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json",
},
{
"/query",
"q=json&take=20",
"https://azuresearch-usnc.nuget.org/query?q=json&take=20",
},
{
"/autocomplete",
"q=new&take=10",
"https://azuresearch-usnc.nuget.org/autocomplete?q=new&take=10",
},
}
for _, tt := range tests {
req := httptest.NewRequest(http.MethodGet, tt.path+"?"+tt.query, nil)
got := h.buildUpstreamURL(req)
if got != tt.want {
t.Errorf("buildUpstreamURL(%q) = %q, want %q", tt.path, got, tt.want)
}
}
}
func TestNuGetRoutes(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v3/index.json" {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"3.0.0","resources":[]}`))
return
}
w.WriteHeader(http.StatusOK)
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
routes := h.Routes()
req := httptest.NewRequest(http.MethodGet, "/v3/index.json", nil)
w := httptest.NewRecorder()
routes.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("GET /v3/index.json: status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestNewNuGetHandler(t *testing.T) {
proxy := nugetTestProxy()
h := NewNuGetHandler(proxy, "http://localhost:8080/")
if h.proxy != proxy {
t.Error("proxy not set correctly")
}
if h.upstreamURL != nugetUpstream {
t.Errorf("upstreamURL = %q, want %q", h.upstreamURL, nugetUpstream)
}
if h.proxyURL != "http://localhost:8080" {
t.Errorf("proxyURL = %q, want %q (trailing slash should be trimmed)", h.proxyURL, "http://localhost:8080")
}
}
func TestNewNuGetHandlerNoTrailingSlash(t *testing.T) {
proxy := nugetTestProxy()
h := NewNuGetHandler(proxy, "http://localhost:8080")
if h.proxyURL != "http://localhost:8080" {
t.Errorf("proxyURL = %q, want %q", h.proxyURL, "http://localhost:8080")
}
}
func TestNuGetRewriteServiceIndexNoResources(t *testing.T) {
h := &NuGetHandler{
proxyURL: "http://localhost:8080",
}
input := `{"version":"3.0.0"}`
output, err := h.rewriteServiceIndex([]byte(input))
if err != nil {
t.Fatalf("rewriteServiceIndex failed: %v", err)
}
// Should return the body unchanged when no resources key
var result map[string]any
if err := json.Unmarshal(output, &result); err != nil {
t.Fatalf("failed to parse output: %v", err)
}
if result["version"] != "3.0.0" {
t.Errorf("version = %v, want 3.0.0", result["version"])
}
}
func TestNuGetRewriteServiceIndexAllTypes(t *testing.T) {
h := &NuGetHandler{
proxyURL: "http://localhost:8080",
}
// Test every rewritable service type
resources := []map[string]string{
{"@id": "https://api.nuget.org/v3-flatcontainer/", "@type": "PackageBaseAddress/3.0.0"},
{"@id": "https://api.nuget.org/v3/registration5-gz-semver2/", "@type": "RegistrationsBaseUrl/3.6.0"},
{"@id": "https://api.nuget.org/v3/registration5-gz-semver2/", "@type": "RegistrationsBaseUrl/Versioned"},
{"@id": "https://azuresearch-usnc.nuget.org/query", "@type": "SearchQueryService"},
{"@id": "https://azuresearch-usnc.nuget.org/query", "@type": "SearchQueryService/3.0.0-rc"},
{"@id": "https://azuresearch-usnc.nuget.org/query", "@type": "SearchQueryService/3.5.0"},
{"@id": "https://azuresearch-usnc.nuget.org/autocomplete", "@type": "SearchAutocompleteService"},
{"@id": "https://azuresearch-usnc.nuget.org/autocomplete", "@type": "SearchAutocompleteService/3.5.0"},
}
inputResources := make([]any, len(resources))
for i, r := range resources {
inputResources[i] = r
}
input, _ := json.Marshal(map[string]any{
"version": "3.0.0",
"resources": inputResources,
})
output, err := h.rewriteServiceIndex(input)
if err != nil {
t.Fatalf("rewriteServiceIndex failed: %v", err)
}
var result map[string]any
if err := json.Unmarshal(output, &result); err != nil {
t.Fatalf("failed to parse output: %v", err)
}
outputResources := result["resources"].([]any)
for _, res := range outputResources {
rmap := res.(map[string]any)
id := rmap["@id"].(string)
// All should be rewritten to proxy URL
if strings.HasPrefix(id, "https://api.nuget.org") || strings.HasPrefix(id, "https://azuresearch-usnc.nuget.org") {
t.Errorf("resource %s was not rewritten: %s", rmap["@type"], id)
}
}
}
func TestNuGetProxyUpstreamPreservesStatusCodes(t *testing.T) {
codes := []int{
http.StatusOK,
http.StatusNotFound,
http.StatusForbidden,
http.StatusInternalServerError,
}
for _, code := range codes {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
}))
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test/index.json", nil)
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
if w.Code != code {
t.Errorf("status = %d, want %d", w.Code, code)
}
upstream.Close()
}
}
func TestNuGetProxyUpstreamCopiesBody(t *testing.T) {
expected := `{"versions":["1.0.0","2.0.0"]}`
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(expected))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test/index.json", nil)
w := httptest.NewRecorder()
h.proxyUpstream(w, req)
got, _ := io.ReadAll(w.Body)
if string(got) != expected {
t.Errorf("body = %q, want %q", string(got), expected)
}
}
func TestNuGetHandleDownloadMissingID(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: "http://localhost:1",
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer//1.0.0/test.nupkg", nil)
req.SetPathValue("id", "")
req.SetPathValue("version", "1.0.0")
req.SetPathValue("filename", "test.nupkg")
w := httptest.NewRecorder()
h.handleDownload(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestNuGetHandleDownloadMissingVersion(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: "http://localhost:1",
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test//test.nupkg", nil)
req.SetPathValue("id", "test")
req.SetPathValue("version", "")
req.SetPathValue("filename", "test.nupkg")
w := httptest.NewRecorder()
h.handleDownload(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestNuGetHandleDownloadMissingFilename(t *testing.T) {
h := &NuGetHandler{
proxy: nugetTestProxy(),
upstreamURL: "http://localhost:1",
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3-flatcontainer/test/1.0.0/", nil)
req.SetPathValue("id", "test")
req.SetPathValue("version", "1.0.0")
req.SetPathValue("filename", "")
w := httptest.NewRecorder()
h.handleDownload(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestNuGetBuildUpstreamURLQueryPath(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
}
// Query endpoint should go to azuresearch
req := httptest.NewRequest(http.MethodGet, "/query?q=json&skip=0&take=20", nil)
got := h.buildUpstreamURL(req)
want := "https://azuresearch-usnc.nuget.org/query?q=json&skip=0&take=20"
if got != want {
t.Errorf("buildUpstreamURL for /query = %q, want %q", got, want)
}
}
func TestNuGetBuildUpstreamURLAutocompletePath(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
}
req := httptest.NewRequest(http.MethodGet, "/autocomplete?q=new&take=10", nil)
got := h.buildUpstreamURL(req)
want := "https://azuresearch-usnc.nuget.org/autocomplete?q=new&take=10"
if got != want {
t.Errorf("buildUpstreamURL for /autocomplete = %q, want %q", got, want)
}
}
func TestNuGetBuildUpstreamURLRegularPath(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
}
req := httptest.NewRequest(http.MethodGet, "/v3/registration5-gz-semver2/newtonsoft.json/index.json", nil)
got := h.buildUpstreamURL(req)
want := "https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json"
if got != want {
t.Errorf("buildUpstreamURL for registration = %q, want %q", got, want)
}
}
func TestNuGetCooldownFiltering(t *testing.T) {
now := time.Now()
oldTime := now.Add(-7 * 24 * time.Hour).Format(time.RFC3339)
recentTime := now.Add(-1 * time.Hour).Format(time.RFC3339)
registration := map[string]any{
"items": []any{
map[string]any{
"count": 2,
"items": []any{
map[string]any{
"catalogEntry": map[string]any{
"id": "TestPackage",
"version": "1.0.0",
"published": oldTime,
},
},
map[string]any{
"catalogEntry": map[string]any{
"id": "TestPackage",
"version": "2.0.0",
"published": recentTime,
},
},
},
},
},
}
body, err := json.Marshal(registration)
if err != nil {
t.Fatal(err)
}
proxy := testProxy()
proxy.Cooldown = &cooldown.Config{
Default: "3d",
}
h := &NuGetHandler{
proxy: proxy,
proxyURL: "http://localhost:8080",
}
filtered, err := h.applyCooldownFiltering(body)
if err != nil {
t.Fatal(err)
}
var result map[string]any
if err := json.Unmarshal(filtered, &result); err != nil {
t.Fatal(err)
}
pages := result["items"].([]any)
page := pages[0].(map[string]any)
items := page["items"].([]any)
if len(items) != 1 {
t.Fatalf("expected 1 item after filtering, got %d", len(items))
}
entry := items[0].(map[string]any)["catalogEntry"].(map[string]any)
if entry["version"] != testVersion100 {
t.Errorf("expected version 1.0.0 to survive, got %s", entry["version"])
}
count := page["count"]
if count != float64(1) {
t.Errorf("expected page count to be 1, got %v", count)
}
}
func TestNuGetCooldownFilteringWithPackageOverride(t *testing.T) {
now := time.Now()
recentTime := now.Add(-2 * time.Hour).Format(time.RFC3339)
registration := map[string]any{
"items": []any{
map[string]any{
"count": 1,
"items": []any{
map[string]any{
"catalogEntry": map[string]any{
"id": "SpecialPackage",
"version": "1.0.0",
"published": recentTime,
},
},
},
},
},
}
body, err := json.Marshal(registration)
if err != nil {
t.Fatal(err)
}
proxy := testProxy()
proxy.Cooldown = &cooldown.Config{
Default: "3d",
Packages: map[string]string{"pkg:nuget/specialpackage": "1h"},
}
h := &NuGetHandler{
proxy: proxy,
proxyURL: "http://localhost:8080",
}
filtered, err := h.applyCooldownFiltering(body)
if err != nil {
t.Fatal(err)
}
var result map[string]any
if err := json.Unmarshal(filtered, &result); err != nil {
t.Fatal(err)
}
pages := result["items"].([]any)
page := pages[0].(map[string]any)
items := page["items"].([]any)
if len(items) != 1 {
t.Fatalf("expected 1 item (package override allows it), got %d", len(items))
}
}
func TestNuGetCooldownNoCooldownConfig(t *testing.T) {
registration := map[string]any{
"items": []any{
map[string]any{
"count": 1,
"items": []any{
map[string]any{
"catalogEntry": map[string]any{
"id": "Test",
"version": "1.0.0",
"published": time.Now().Format(time.RFC3339),
},
},
},
},
},
}
body, err := json.Marshal(registration)
if err != nil {
t.Fatal(err)
}
// No cooldown - applyCooldownFiltering still works, just doesn't filter
h := &NuGetHandler{
proxy: testProxy(),
proxyURL: "http://localhost:8080",
}
filtered, err := h.applyCooldownFiltering(body)
if err != nil {
t.Fatal(err)
}
var result map[string]any
if err := json.Unmarshal(filtered, &result); err != nil {
t.Fatal(err)
}
pages := result["items"].([]any)
page := pages[0].(map[string]any)
items := page["items"].([]any)
// Without cooldown config on the handler, applyCooldownFiltering
// is called but proxy.Cooldown is nil, so IsAllowed is never called
// Actually, applyCooldownFiltering always runs the filter logic -
// but the caller (handleRegistration) short-circuits when cooldown is disabled.
// The function itself should still work fine with a nil Cooldown.
if len(items) != 1 {
t.Fatalf("expected 1 item, got %d", len(items))
}
}
func TestNuGetCooldownFilteringNuGetTimestamp(t *testing.T) {
// NuGet uses timestamps like "2024-09-07T01:37:52.233+00:00" which
// have fractional seconds - verify these parse correctly
now := time.Now()
oldTime := now.Add(-7 * 24 * time.Hour).Format("2006-01-02T15:04:05.000-07:00")
registration := map[string]any{
"items": []any{
map[string]any{
"count": 1,
"items": []any{
map[string]any{
"catalogEntry": map[string]any{
"id": "Test",
"version": "1.0.0",
"published": oldTime,
},
},
},
},
},
}
body, err := json.Marshal(registration)
if err != nil {
t.Fatal(err)
}
proxy := testProxy()
proxy.Cooldown = &cooldown.Config{
Default: "3d",
}
h := &NuGetHandler{
proxy: proxy,
proxyURL: "http://localhost:8080",
}
filtered, err := h.applyCooldownFiltering(body)
if err != nil {
t.Fatal(err)
}
var result map[string]any
if err := json.Unmarshal(filtered, &result); err != nil {
t.Fatal(err)
}
pages := result["items"].([]any)
page := pages[0].(map[string]any)
items := page["items"].([]any)
if len(items) != 1 {
t.Fatalf("expected 1 item (old enough to pass cooldown), got %d", len(items))
}
}
func TestNuGetHandleRegistrationWithCooldown(t *testing.T) {
now := time.Now()
oldTime := now.Add(-7 * 24 * time.Hour).Format(time.RFC3339)
recentTime := now.Add(-1 * time.Hour).Format(time.RFC3339)
registrationJSON, _ := json.Marshal(map[string]any{
"items": []any{
map[string]any{
"count": 2,
"items": []any{
map[string]any{
"catalogEntry": map[string]any{
"id": "TestPkg",
"version": "1.0.0",
"published": oldTime,
},
},
map[string]any{
"catalogEntry": map[string]any{
"id": "TestPkg",
"version": "2.0.0",
"published": recentTime,
},
},
},
},
},
})
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(registrationJSON)
}))
defer upstream.Close()
proxy := testProxy()
proxy.Cooldown = &cooldown.Config{
Default: "3d",
}
h := &NuGetHandler{
proxy: proxy,
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3/registration5-gz-semver2/testpkg/index.json", nil)
w := httptest.NewRecorder()
h.handleRegistration(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
}
var result map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
pages := result["items"].([]any)
page := pages[0].(map[string]any)
items := page["items"].([]any)
if len(items) != 1 {
t.Fatalf("expected 1 item after cooldown filtering, got %d", len(items))
}
}
func TestNuGetHandleRegistrationWithoutCooldown(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"items":[]}`))
}))
defer upstream.Close()
h := &NuGetHandler{
proxy: nugetTestProxy(), // no cooldown configured
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/v3/registration5-gz-semver2/testpkg/index.json", nil)
w := httptest.NewRecorder()
h.handleRegistration(w, req)
// Without cooldown, should proxy directly
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
}
}