pkg-proxy/internal/handler/conan_test.go
Andrew Nesbitt 599fe9e254
Fix all golangci-lint issues across the codebase (#32)
* 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.
2026-03-18 10:59:29 +00:00

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()
}
}