forked from mirrors/pkg-proxy
* Fix all golangci-lint issues across the codebase Resolve 77 lint issues reported by golangci-lint with gocritic, gocognit, gocyclo, maintidx, dupl, mnd, unparam, ireturn, goconst, and errcheck enabled. Net reduction of ~175 lines through shared helpers and deduplication. * Suppress staticcheck SA1019 for intentional deprecated field usage The Storage.Path field is deprecated but still read for backwards compatibility with existing configs that haven't migrated to the URL field.
476 lines
12 KiB
Go
476 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const testProxyURL = "http://localhost:8080"
|
|
|
|
func conanTestProxy() *Proxy {
|
|
return &Proxy{
|
|
Logger: slog.Default(),
|
|
HTTPClient: http.DefaultClient,
|
|
}
|
|
}
|
|
|
|
func TestConanShouldCacheFile(t *testing.T) {
|
|
h := &ConanHandler{}
|
|
|
|
tests := []struct {
|
|
filename string
|
|
want bool
|
|
}{
|
|
{"conan_sources.tgz", true},
|
|
{"conan_export.tgz", true},
|
|
{"conan_package.tgz", true},
|
|
{"conanfile.py", false},
|
|
{"conanmanifest.txt", false},
|
|
{"conaninfo.txt", false},
|
|
{"random.tgz", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := h.shouldCacheFile(tt.filename)
|
|
if got != tt.want {
|
|
t.Errorf("shouldCacheFile(%q) = %v, want %v", tt.filename, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConanPingV1(t *testing.T) {
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
proxyURL: testProxyURL,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/ping", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handlePing(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
caps := w.Header().Get("X-Conan-Server-Capabilities")
|
|
if caps != "revisions" {
|
|
t.Errorf("X-Conan-Server-Capabilities = %q, want %q", caps, "revisions")
|
|
}
|
|
}
|
|
|
|
func TestConanPingV2(t *testing.T) {
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
proxyURL: testProxyURL,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/ping", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handlePing(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
caps := w.Header().Get("X-Conan-Server-Capabilities")
|
|
if caps != "revisions" {
|
|
t.Errorf("X-Conan-Server-Capabilities = %q, want %q", caps, "revisions")
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstream(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v2/conans/search" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
if r.URL.Query().Get("q") != "zlib" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"results":["zlib/1.2.13"]}`))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/search?q=zlib", 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, "zlib/1.2.13") {
|
|
t.Errorf("response body does not contain expected result: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamNotFound(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamCopiesHeaders(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Custom-Header", "test-value")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/test", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Header().Get("X-Custom-Header") != "test-value" {
|
|
t.Errorf("X-Custom-Header = %q, want %q", w.Header().Get("X-Custom-Header"), "test-value")
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamForwardsAuthHeader(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth != "Bearer mytoken" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/test", nil)
|
|
req.Header.Set("Authorization", "Bearer mytoken")
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamBadUpstream(t *testing.T) {
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: "http://127.0.0.1:1", // unreachable
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/test", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusBadGateway {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadGateway)
|
|
}
|
|
}
|
|
|
|
func TestConanRecipeFileNonCacheable(t *testing.T) {
|
|
// When a recipe file is not cacheable (e.g. conanfile.py), it should be proxied upstream.
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("conanfile content"))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/files/zlib/1.2.13/_/_/abc123/recipe/conanfile.py", nil)
|
|
req.SetPathValue("name", "zlib")
|
|
req.SetPathValue("version", "1.2.13")
|
|
req.SetPathValue("user", "_")
|
|
req.SetPathValue("channel", "_")
|
|
req.SetPathValue("revision", "abc123")
|
|
req.SetPathValue("filename", "conanfile.py")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.handleRecipeFile(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if body != "conanfile content" {
|
|
t.Errorf("body = %q, want %q", body, "conanfile content")
|
|
}
|
|
}
|
|
|
|
func TestConanPackageFileNonCacheable(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("conaninfo content"))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/files/zlib/1.2.13/_/_/abc123/package/pkgref1/pkgrev1/conaninfo.txt", nil)
|
|
req.SetPathValue("name", "zlib")
|
|
req.SetPathValue("version", "1.2.13")
|
|
req.SetPathValue("user", "_")
|
|
req.SetPathValue("channel", "_")
|
|
req.SetPathValue("revision", "abc123")
|
|
req.SetPathValue("pkgref", "pkgref1")
|
|
req.SetPathValue("pkgrev", "pkgrev1")
|
|
req.SetPathValue("filename", "conaninfo.txt")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.handlePackageFile(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if body != "conaninfo content" {
|
|
t.Errorf("body = %q, want %q", body, "conaninfo content")
|
|
}
|
|
}
|
|
|
|
func TestConanRoutes(t *testing.T) {
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: "http://localhost:1", // won't be called for ping
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
routes := h.Routes()
|
|
|
|
tests := []struct {
|
|
path string
|
|
wantStatus int
|
|
}{
|
|
{"/v1/ping", http.StatusOK},
|
|
{"/v2/ping", http.StatusOK},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
|
w := httptest.NewRecorder()
|
|
routes.ServeHTTP(w, req)
|
|
|
|
if w.Code != tt.wantStatus {
|
|
t.Errorf("GET %s: status = %d, want %d", tt.path, w.Code, tt.wantStatus)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamPreservesQueryString(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("q") != "boost" && r.URL.Query().Get("page") != "2" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`ok`))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/search?q=boost&page=2", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamLargeResponse(t *testing.T) {
|
|
largeBody := strings.Repeat("x", 1024*1024)
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(largeBody))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/test", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if w.Body.Len() != len(largeBody) {
|
|
t.Errorf("body length = %d, want %d", w.Body.Len(), len(largeBody))
|
|
}
|
|
}
|
|
|
|
func TestNewConanHandler(t *testing.T) {
|
|
proxy := conanTestProxy()
|
|
h := NewConanHandler(proxy, "http://localhost:8080/")
|
|
|
|
if h.proxy != proxy {
|
|
t.Error("proxy not set correctly")
|
|
}
|
|
if h.upstreamURL != conanUpstream {
|
|
t.Errorf("upstreamURL = %q, want %q", h.upstreamURL, conanUpstream)
|
|
}
|
|
if h.proxyURL != testProxyURL {
|
|
t.Errorf("proxyURL = %q, want %q (trailing slash should be trimmed)", h.proxyURL, testProxyURL)
|
|
}
|
|
}
|
|
|
|
func TestNewConanHandlerNoTrailingSlash(t *testing.T) {
|
|
proxy := conanTestProxy()
|
|
h := NewConanHandler(proxy, testProxyURL)
|
|
|
|
if h.proxyURL != testProxyURL {
|
|
t.Errorf("proxyURL = %q, want %q", h.proxyURL, testProxyURL)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamNoAuthHeaderWhenNotProvided(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth != "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte("unexpected auth header"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/test", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamCopiesBody(t *testing.T) {
|
|
expected := `{"name":"zlib","version":"1.2.13","user":"_","channel":"_"}`
|
|
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 := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/conans/zlib/1.2.13/_/_/latest", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
got, _ := io.ReadAll(w.Body)
|
|
if string(got) != expected {
|
|
t.Errorf("body = %q, want %q", string(got), expected)
|
|
}
|
|
}
|
|
|
|
func TestConanProxyUpstreamPreservesStatusCodes(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 := &ConanHandler{
|
|
proxy: conanTestProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test", nil)
|
|
w := httptest.NewRecorder()
|
|
h.proxyUpstream(w, req)
|
|
|
|
if w.Code != code {
|
|
t.Errorf("status = %d, want %d", w.Code, code)
|
|
}
|
|
|
|
upstream.Close()
|
|
}
|
|
}
|