mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-08 23:09:33 +00:00
386 lines
9.5 KiB
Go
386 lines
9.5 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/lukaszraczylo/gohoarder/pkg/cache"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestNewBaseHandler tests base handler creation
|
|
func TestNewBaseHandler(t *testing.T) {
|
|
// Use nil for cache and client since we're only testing structure
|
|
handler := NewBaseHandler(nil, nil, "npm", "https://registry.npmjs.org")
|
|
|
|
require.NotNil(t, handler)
|
|
assert.Equal(t, "npm", handler.Registry)
|
|
assert.Equal(t, "https://registry.npmjs.org", handler.Upstream)
|
|
assert.Nil(t, handler.Cache)
|
|
assert.Nil(t, handler.Client)
|
|
}
|
|
|
|
// TestGetRegistry tests registry type retrieval
|
|
func TestGetRegistry(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
registry string
|
|
}{
|
|
{"npm registry", "npm"},
|
|
{"pypi registry", "pypi"},
|
|
{"go registry", "go"},
|
|
{"custom registry", "custom"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
handler := &BaseHandler{Registry: tt.registry}
|
|
assert.Equal(t, tt.registry, handler.GetRegistry())
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleUpstreamError tests upstream error handling
|
|
func TestHandleUpstreamError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
url string
|
|
context string
|
|
wantStatus int
|
|
wantContain string
|
|
}{
|
|
// GOOD: Standard error
|
|
{
|
|
name: "connection error",
|
|
err: errors.New("connection refused"),
|
|
url: "https://registry.npmjs.org/react",
|
|
context: "package",
|
|
wantStatus: http.StatusBadGateway,
|
|
wantContain: "Failed to fetch package",
|
|
},
|
|
// WRONG: Timeout error
|
|
{
|
|
name: "timeout error",
|
|
err: context.DeadlineExceeded,
|
|
url: "https://registry.npmjs.org/lodash",
|
|
context: "metadata",
|
|
wantStatus: http.StatusBadGateway,
|
|
wantContain: "Failed to fetch metadata",
|
|
},
|
|
// EDGE: Empty context
|
|
{
|
|
name: "empty context",
|
|
err: errors.New("error"),
|
|
url: "https://example.com",
|
|
context: "",
|
|
wantStatus: http.StatusBadGateway,
|
|
wantContain: "Failed to fetch",
|
|
},
|
|
// EDGE: Long URL
|
|
{
|
|
name: "long URL",
|
|
err: errors.New("error"),
|
|
url: "https://registry.npmjs.org/@scope/very-long-package-name/versions/1.2.3",
|
|
context: "package",
|
|
wantStatus: http.StatusBadGateway,
|
|
wantContain: "Failed to fetch package",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
HandleUpstreamError(w, tt.err, tt.url, tt.context)
|
|
|
|
assert.Equal(t, tt.wantStatus, w.Code)
|
|
assert.Contains(t, w.Body.String(), tt.wantContain)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCheckUpstreamStatus tests upstream status validation
|
|
func TestCheckUpstreamStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
statusCode int
|
|
body io.ReadCloser
|
|
wantErr bool
|
|
errContains string
|
|
bodyClosed bool
|
|
}{
|
|
// GOOD: OK status
|
|
{
|
|
name: "200 OK",
|
|
statusCode: http.StatusOK,
|
|
body: io.NopCloser(strings.NewReader("success")),
|
|
wantErr: false,
|
|
},
|
|
// WRONG: Not found
|
|
{
|
|
name: "404 Not Found",
|
|
statusCode: http.StatusNotFound,
|
|
body: io.NopCloser(strings.NewReader("not found")),
|
|
wantErr: true,
|
|
errContains: "upstream returned status 404",
|
|
},
|
|
// WRONG: Server error
|
|
{
|
|
name: "500 Internal Server Error",
|
|
statusCode: http.StatusInternalServerError,
|
|
body: io.NopCloser(strings.NewReader("error")),
|
|
wantErr: true,
|
|
errContains: "upstream returned status 500",
|
|
},
|
|
// BAD: Unauthorized
|
|
{
|
|
name: "401 Unauthorized",
|
|
statusCode: http.StatusUnauthorized,
|
|
body: io.NopCloser(strings.NewReader("unauthorized")),
|
|
wantErr: true,
|
|
errContains: "upstream returned status 401",
|
|
},
|
|
// EDGE: Nil body
|
|
{
|
|
name: "nil body with error",
|
|
statusCode: http.StatusNotFound,
|
|
body: nil,
|
|
wantErr: true,
|
|
errContains: "upstream returned status 404",
|
|
},
|
|
// EDGE: Redirect status
|
|
{
|
|
name: "302 Found",
|
|
statusCode: http.StatusFound,
|
|
body: io.NopCloser(strings.NewReader("redirect")),
|
|
wantErr: true,
|
|
errContains: "upstream returned status 302",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := CheckUpstreamStatus(tt.statusCode, tt.body)
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.errContains)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleInvalidRequest tests invalid request handling
|
|
func TestHandleInvalidRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
registry string
|
|
wantStatus int
|
|
wantContain string
|
|
}{
|
|
{
|
|
name: "npm invalid request",
|
|
registry: "npm",
|
|
wantStatus: http.StatusBadRequest,
|
|
wantContain: "Invalid npm request",
|
|
},
|
|
{
|
|
name: "pypi invalid request",
|
|
registry: "pypi",
|
|
wantStatus: http.StatusBadRequest,
|
|
wantContain: "Invalid pypi request",
|
|
},
|
|
{
|
|
name: "go invalid request",
|
|
registry: "go",
|
|
wantStatus: http.StatusBadRequest,
|
|
wantContain: "Invalid go request",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
HandleInvalidRequest(w, tt.registry)
|
|
|
|
assert.Equal(t, tt.wantStatus, w.Code)
|
|
assert.Contains(t, w.Body.String(), tt.wantContain)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleInternalError tests internal error handling
|
|
func TestHandleInternalError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
context string
|
|
wantStatus int
|
|
wantContain string
|
|
}{
|
|
{
|
|
name: "database error",
|
|
err: errors.New("database connection failed"),
|
|
context: "database",
|
|
wantStatus: http.StatusInternalServerError,
|
|
wantContain: "Internal error: database",
|
|
},
|
|
{
|
|
name: "cache error",
|
|
err: errors.New("cache write failed"),
|
|
context: "cache",
|
|
wantStatus: http.StatusInternalServerError,
|
|
wantContain: "Internal error: cache",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
HandleInternalError(w, tt.err, tt.context)
|
|
|
|
assert.Equal(t, tt.wantStatus, w.Code)
|
|
assert.Contains(t, w.Body.String(), tt.wantContain)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Note: FetchFromUpstream tests would require mocking cache.Manager and network.Client
|
|
// which requires concrete implementations. Integration tests cover this functionality.
|
|
|
|
// TestWriteResponse tests HTTP response writing
|
|
func TestWriteResponse(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
contentType string
|
|
wantStatus int
|
|
wantBody string
|
|
wantErr bool
|
|
}{
|
|
// GOOD: Write tarball
|
|
{
|
|
name: "write tarball",
|
|
data: "package data here",
|
|
contentType: "application/octet-stream",
|
|
wantStatus: http.StatusOK,
|
|
wantBody: "package data here",
|
|
wantErr: false,
|
|
},
|
|
// GOOD: Write JSON
|
|
{
|
|
name: "write JSON metadata",
|
|
data: `{"name":"react","version":"18.2.0"}`,
|
|
contentType: "application/json",
|
|
wantStatus: http.StatusOK,
|
|
wantBody: `{"name":"react","version":"18.2.0"}`,
|
|
wantErr: false,
|
|
},
|
|
// EDGE: Empty data
|
|
{
|
|
name: "empty data",
|
|
data: "",
|
|
contentType: "text/plain",
|
|
wantStatus: http.StatusOK,
|
|
wantBody: "",
|
|
wantErr: false,
|
|
},
|
|
// EDGE: Large data
|
|
{
|
|
name: "large data",
|
|
data: strings.Repeat("x", 100000),
|
|
contentType: "application/octet-stream",
|
|
wantStatus: http.StatusOK,
|
|
wantBody: strings.Repeat("x", 100000),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
entry := &cache.CacheEntry{
|
|
Data: io.NopCloser(bytes.NewReader([]byte(tt.data))),
|
|
}
|
|
|
|
err := WriteResponse(w, entry, tt.contentType)
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.contentType, w.Header().Get("Content-Type"))
|
|
assert.Equal(t, tt.wantBody, w.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBaseHandlerFields tests that BaseHandler fields are properly set
|
|
func TestBaseHandlerFields(t *testing.T) {
|
|
handler := NewBaseHandler(nil, nil, "npm", "https://registry.npmjs.org")
|
|
|
|
tests := []struct {
|
|
name string
|
|
field string
|
|
expected interface{}
|
|
}{
|
|
{"registry field", "registry", "npm"},
|
|
{"upstream field", "upstream", "https://registry.npmjs.org"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
switch tt.field {
|
|
case "registry":
|
|
assert.Equal(t, tt.expected, handler.Registry)
|
|
case "upstream":
|
|
assert.Equal(t, tt.expected, handler.Upstream)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestProxyHandlerInterface tests that BaseHandler can be used as ProxyHandler
|
|
func TestProxyHandlerInterface(t *testing.T) {
|
|
handler := NewBaseHandler(nil, nil, "npm", "https://registry.npmjs.org")
|
|
|
|
// Verify GetRegistry works
|
|
registry := handler.GetRegistry()
|
|
assert.Equal(t, "npm", registry)
|
|
}
|
|
|
|
// TestConcurrentWriteResponse tests that WriteResponse is safe for concurrent use
|
|
func TestConcurrentWriteResponse(t *testing.T) {
|
|
const numGoroutines = 10
|
|
|
|
errs := make(chan error, numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(n int) {
|
|
w := httptest.NewRecorder()
|
|
data := strings.Repeat("x", 1000)
|
|
entry := &cache.CacheEntry{
|
|
Data: io.NopCloser(bytes.NewReader([]byte(data))),
|
|
}
|
|
|
|
err := WriteResponse(w, entry, "text/plain")
|
|
errs <- err
|
|
}(i)
|
|
}
|
|
|
|
// Collect results
|
|
for i := 0; i < numGoroutines; i++ {
|
|
err := <-errs
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|