2026-01-20 21:52:44 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-06 11:11:00 +01:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2026-01-20 21:52:44 +00:00
|
|
|
"log/slog"
|
2026-04-06 11:11:00 +01:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
2026-01-20 21:52:44 +00:00
|
|
|
"testing"
|
2026-04-06 11:11:00 +01:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/cooldown"
|
2026-01-20 21:52:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestGemParseFilename(t *testing.T) {
|
|
|
|
|
h := &GemHandler{proxy: &Proxy{Logger: slog.Default()}}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
filename string
|
|
|
|
|
wantName string
|
|
|
|
|
wantVersion string
|
|
|
|
|
}{
|
|
|
|
|
{"rails-7.1.0.gem", "rails", "7.1.0"},
|
|
|
|
|
{"aws-sdk-s3-1.142.0.gem", "aws-sdk-s3", "1.142.0"},
|
|
|
|
|
{"nokogiri-1.15.4-x86_64-linux.gem", "nokogiri", "1.15.4-x86_64-linux"},
|
|
|
|
|
{"activerecord-7.0.8.gem", "activerecord", "7.0.8"},
|
|
|
|
|
{"invalid", "", ""},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
name, version := h.parseGemFilename(tt.filename)
|
|
|
|
|
if name != tt.wantName || version != tt.wantVersion {
|
|
|
|
|
t.Errorf("parseGemFilename(%q) = (%q, %q), want (%q, %q)",
|
|
|
|
|
tt.filename, name, version, tt.wantName, tt.wantVersion)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-06 11:11:00 +01:00
|
|
|
|
|
|
|
|
func TestGemCompactIndexCooldown(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)
|
|
|
|
|
|
|
|
|
|
compactIndex := "---\n1.0.0 dep1:>= 1.0|checksum:abc123\n2.0.0 dep1:>= 1.0|checksum:def456\n"
|
|
|
|
|
|
|
|
|
|
versionsJSON, _ := json.Marshal([]gemVersion{
|
|
|
|
|
{Number: "1.0.0", Platform: "ruby", CreatedAt: oldTime},
|
|
|
|
|
{Number: "2.0.0", Platform: "ruby", CreatedAt: recentTime},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/info/"):
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
|
_, _ = w.Write([]byte(compactIndex))
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/api/v1/versions/"):
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write(versionsJSON)
|
|
|
|
|
default:
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy := testProxy()
|
|
|
|
|
proxy.Cooldown = &cooldown.Config{
|
|
|
|
|
Default: "3d",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h := &GemHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/info/testgem", nil)
|
|
|
|
|
req.SetPathValue("name", "testgem")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.handleCompactIndex(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body := w.Body.String()
|
|
|
|
|
if !strings.Contains(body, "1.0.0") {
|
|
|
|
|
t.Error("expected version 1.0.0 to survive filtering")
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(body, "2.0.0") {
|
|
|
|
|
t.Error("expected version 2.0.0 to be filtered out")
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(body, "---\n") {
|
|
|
|
|
t.Error("expected compact index header to be preserved")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGemCompactIndexCooldownWithPlatformVersion(t *testing.T) {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
recentTime := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
|
|
|
|
|
|
|
|
compactIndex := "---\n1.0.0 dep:>= 1.0|checksum:abc\n1.0.0-java dep:>= 1.0|checksum:def\n"
|
|
|
|
|
|
|
|
|
|
versionsJSON, _ := json.Marshal([]gemVersion{
|
|
|
|
|
{Number: "1.0.0", Platform: "ruby", CreatedAt: recentTime},
|
|
|
|
|
{Number: "1.0.0", Platform: "java", CreatedAt: recentTime},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/info/"):
|
|
|
|
|
_, _ = w.Write([]byte(compactIndex))
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/api/v1/versions/"):
|
|
|
|
|
_, _ = w.Write(versionsJSON)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy := testProxy()
|
|
|
|
|
proxy.Cooldown = &cooldown.Config{
|
|
|
|
|
Default: "3d",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h := &GemHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/info/testgem", nil)
|
|
|
|
|
req.SetPathValue("name", "testgem")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.handleCompactIndex(w, req)
|
|
|
|
|
|
|
|
|
|
body := w.Body.String()
|
|
|
|
|
// Both ruby and java platform versions should be filtered
|
|
|
|
|
lines := strings.Split(strings.TrimSpace(body), "\n")
|
|
|
|
|
if len(lines) != 1 { // only "---"
|
|
|
|
|
t.Errorf("expected only header line, got %d lines: %v", len(lines), lines)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGemCompactIndexNoCooldown(t *testing.T) {
|
|
|
|
|
compactIndex := "---\n1.0.0 dep:>= 1.0|checksum:abc\n"
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
_, _ = w.Write([]byte(compactIndex))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
h := &GemHandler{
|
|
|
|
|
proxy: testProxy(), // no cooldown
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/info/testgem", nil)
|
|
|
|
|
req.SetPathValue("name", "testgem")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.handleCompactIndex(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGemCompactIndexVersionsAPIFails(t *testing.T) {
|
|
|
|
|
compactIndex := "---\n1.0.0 dep:>= 1.0|checksum:abc\n"
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/info/"):
|
|
|
|
|
_, _ = w.Write([]byte(compactIndex))
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/api/v1/versions/"):
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy := testProxy()
|
|
|
|
|
proxy.Cooldown = &cooldown.Config{
|
|
|
|
|
Default: "3d",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h := &GemHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/info/testgem", nil)
|
|
|
|
|
req.SetPathValue("name", "testgem")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.handleCompactIndex(w, req)
|
|
|
|
|
|
|
|
|
|
// Should still return OK with unfiltered content
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body := w.Body.String()
|
|
|
|
|
if !strings.Contains(body, "1.0.0") {
|
|
|
|
|
t.Error("expected unfiltered content when versions API fails")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGemFetchFilteredVersions(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)
|
|
|
|
|
|
|
|
|
|
versionsJSON, _ := json.Marshal([]gemVersion{
|
|
|
|
|
{Number: "1.0.0", Platform: "ruby", CreatedAt: oldTime},
|
|
|
|
|
{Number: "2.0.0", Platform: "ruby", CreatedAt: recentTime},
|
|
|
|
|
{Number: "2.0.0", Platform: "java", CreatedAt: recentTime},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write(versionsJSON)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy := testProxy()
|
|
|
|
|
proxy.Cooldown = &cooldown.Config{
|
|
|
|
|
Default: "3d",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h := &GemHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/info/testgem", nil)
|
|
|
|
|
filtered, err := h.fetchFilteredVersions(req, "testgem")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if filtered["1.0.0"] {
|
|
|
|
|
t.Error("version 1.0.0 should not be filtered (old enough)")
|
|
|
|
|
}
|
|
|
|
|
if !filtered["2.0.0"] {
|
|
|
|
|
t.Error("version 2.0.0 (ruby) should be filtered")
|
|
|
|
|
}
|
|
|
|
|
if !filtered["2.0.0-java"] {
|
|
|
|
|
t.Error("version 2.0.0-java should be filtered")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = fmt.Sprintf // silence unused import
|
|
|
|
|
}
|