Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 16:19:16 -05:00
pkg-proxy/internal/server/templates_test.go
simon chrzanowski 832c800ea5
Serve diff2html from the proxy instead of jsDelivr
The compare page loaded diff2html from cdn.jsdelivr.net without a version
or integrity hash, so whatever jsDelivr served as the latest release ran
with the proxy origin's privileges, and the page broke where the proxy
cannot reach the internet.

- Vendor diff2html 3.4.56 (MIT) under static/vendor like tailwind and lucide
- Load its script and stylesheet from /ui/static/vendor
- Fail a test when any template loads a script or stylesheet from another origin
2026-10-14 08:37:43 +02:00

738 lines
20 KiB
Go

package server
import (
"database/sql"
"io/fs"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"github.com/git-pkgs/proxy/internal/database"
)
func TestTemplatesRenderAllPages(t *testing.T) {
templates := &Templates{}
tests := []struct {
page string
data any
}{
{"dashboard", DashboardData{
Stats: DashboardStats{
CachedArtifacts: 42,
TotalSize: "1.5 GB",
TotalPackages: 10,
TotalVersions: 25,
},
EnrichmentStats: EnrichmentStatsView{
EnrichedPackages: 5,
TotalVulnerabilities: 3,
CriticalVulns: 1,
HasVulns: true,
},
PopularPackages: []PackageInfo{
{Ecosystem: "npm", Name: "lodash", Hits: 100, Size: "500 KB", License: "MIT", LicenseCategory: "permissive"},
},
RecentPackages: []PackageInfo{
{Ecosystem: "cargo", Name: "serde", Version: "1.0.0", Size: "200 KB", CachedAt: "1 hour ago"},
},
}},
{"analytics", AnalyticsData{
Totals: AnalyticsTotals{
DownloadedBytes: 1_500_000_000,
Downloaded: "1.4 GB",
Downloads: "12,004",
CacheSize: "420.0 MB",
CachedArtifacts: "1,204",
Packages: "310",
Versions: "902",
Ecosystems: 3,
ActiveEcosystems: 2,
Amplification: "3.4x",
},
EnrichmentStats: EnrichmentStatsView{TotalVulnerabilities: 3, CriticalVulns: 1, HasVulns: true},
Ecosystems: []EcosystemRow{
{Ecosystem: "npm", DownloadedBytes: 1_000_000_000, Downloaded: "953.7 MB", Downloads: "9,000", CacheSize: "300.0 MB", AvgArtifactSize: "120 KB", Artifacts: "900", Packages: "200", Versions: "700", SharePct: "66.7"},
{Ecosystem: "cargo", DownloadedBytes: 500_000_000, Downloaded: "476.8 MB", Downloads: "3,004", CacheSize: "120.0 MB", AvgArtifactSize: "400 KB", Artifacts: "300", Packages: "100", Versions: "200", SharePct: "33.3"},
{Ecosystem: "rpm", Downloaded: "0 B", Downloads: "0", CacheSize: "0 B", AvgArtifactSize: "0 B", Artifacts: "0", Packages: "10", Versions: "2", SharePct: "0.0"},
},
Donut: donutView([]database.EcosystemStats{
{Ecosystem: "npm", CacheSize: 300, Downloads: 9000, DownloadedBytes: 1_000_000_000},
{Ecosystem: "cargo", CacheSize: 120, Downloads: 3004, DownloadedBytes: 500_000_000},
}, 1_500_000_000, "1.4 GB"),
Runtime: RuntimeView{
Available: true,
Requests: "12,004",
ActiveRequests: "2",
RequestMean: "8.1 ms",
CacheHits: "9,000",
CacheMisses: "1,000",
CacheHitRatio: "90.0",
HasCacheTraffic: true,
StatusClasses: []LabelledCount{{Label: "2xx", Count: "11,900"}, {Label: "5xx", Count: "104", Bad: true}},
StorageOps: []LabelledStat{{Label: "get", Count: "9,000", Mean: "412 µs"}},
ScanErrors: []LabelledCount{{Label: "npm · clamav · timeout", Count: "3", Bad: true}},
ScanningOn: true,
Breakers: []BreakerRow{{Registry: "registry.npmjs.org", State: "closed"}},
},
}},
{"analytics", AnalyticsData{}},
{"install", struct {
Layout
BaseURL string
Registries []RegistryConfig
}{
BaseURL: "http://localhost:8080",
Registries: getRegistryConfigs("http://localhost:8080"),
}},
{"search", SearchPageData{
Query: "lodash",
Ecosystem: "npm",
Results: []SearchResultItem{{Ecosystem: "npm", Name: "lodash", LatestVersion: "4.17.21", Hits: 50, SizeFormatted: "1 MB"}},
Count: 1,
Page: 1,
PerPage: 50,
TotalPages: 1,
}},
{"search", SearchPageData{
Query: "nothing",
Results: []SearchResultItem{},
Count: 0,
Page: 1,
PerPage: 50,
TotalPages: 0,
}},
{"packages_list", PackagesListPageData{
Ecosystem: "",
SortBy: defaultSortBy,
Results: []SearchResultItem{{Ecosystem: "npm", Name: "express", Hits: 200, SizeFormatted: "2 MB"}},
Count: 1,
TotalPackages: 1,
EcosystemFilters: []EcosystemFilter{{Ecosystem: "npm", Count: 1}},
Page: 1,
PerPage: 50,
TotalPages: 1,
}},
{"package_show", PackageShowData{
Package: &database.Package{
PURL: "pkg:npm/lodash",
Ecosystem: "npm",
Name: "lodash",
LatestVersion: sql.NullString{String: "4.17.21", Valid: true},
License: sql.NullString{String: "MIT", Valid: true},
},
Versions: []database.Version{
{PURL: "pkg:npm/lodash@4.17.21", PackagePURL: "pkg:npm/lodash"},
},
Vulnerabilities: []database.Vulnerability{},
LicenseCategory: "permissive",
}},
{"package_show", PackageShowData{
Package: &database.Package{
PURL: "pkg:npm/minimal",
Ecosystem: "npm",
Name: "minimal",
},
Versions: []database.Version{},
Vulnerabilities: []database.Vulnerability{},
LicenseCategory: "unknown",
}},
{"version_show", VersionShowData{
Package: &database.Package{
PURL: "pkg:npm/lodash",
Ecosystem: "npm",
Name: "lodash",
LatestVersion: sql.NullString{String: "4.17.21", Valid: true},
License: sql.NullString{String: "MIT", Valid: true},
},
Version: &database.Version{
PURL: "pkg:npm/lodash@4.17.20",
PackagePURL: "pkg:npm/lodash",
},
Artifacts: []database.Artifact{
{
VersionPURL: "pkg:npm/lodash@4.17.20",
Filename: "lodash-4.17.20.tgz",
StoragePath: sql.NullString{String: "npm/lodash/4.17.20/lodash-4.17.20.tgz", Valid: true},
Size: sql.NullInt64{Int64: 1024, Valid: true},
HitCount: 42,
},
},
Vulnerabilities: []database.Vulnerability{},
IsOutdated: true,
LicenseCategory: "permissive",
HasCachedArtifact: true,
}},
{"browse_source", BrowseSourceData{
Ecosystem: "npm",
PackageName: "lodash",
Version: "4.17.21",
}},
{"compare_versions", ComparePageData{
Ecosystem: "npm",
PackageName: "lodash",
FromVersion: "4.17.20",
ToVersion: "4.17.21",
}},
}
for _, tt := range tests {
t.Run(tt.page, func(t *testing.T) {
w := httptest.NewRecorder()
err := templates.Render(w, tt.page, tt.data)
if err != nil {
t.Fatalf("Render(%q) failed: %v", tt.page, err)
}
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
if w.Header().Get("Content-Type") != "text/html; charset=utf-8" {
t.Errorf("Content-Type = %q, want text/html", w.Header().Get("Content-Type"))
}
body := w.Body.String()
if body == "" {
t.Error("rendered page is empty")
}
if !strings.Contains(body, "<!DOCTYPE html>") && !strings.Contains(body, "<html") {
t.Error("rendered page doesn't look like HTML")
}
})
}
}
func TestRenderEmitsCanonicalAndOG(t *testing.T) {
templates := &Templates{}
data := DashboardData{
Layout: Layout{
UIBaseURL: "https://ui.example.com/ui",
CanonicalPath: "/ui/",
},
}
w := httptest.NewRecorder()
if err := templates.Render(w, "dashboard", data); err != nil {
t.Fatalf("Render failed: %v", err)
}
body := w.Body.String()
want := []string{
`<link rel="canonical" href="https://ui.example.com/ui/ui/">`,
`<meta property="og:url" content="https://ui.example.com/ui/ui/">`,
`<meta property="og:site_name" content="git-pkgs proxy">`,
}
for _, s := range want {
if !strings.Contains(body, s) {
t.Errorf("rendered body missing %q", s)
}
}
}
func TestFooterUsesBuildInfoWhenPageDefinesVersion(t *testing.T) {
templates := &Templates{}
buildInfo := BuildInfo{Version: "proxy-build-1.2.3", Commit: "abc123def"}
wantFooter := "proxy proxy-build-1.2.3 (abc123def)"
tests := []struct {
name string
page string
data any
shadow string
}{
{
name: "version show page",
page: "version_show",
data: VersionShowData{
Layout: Layout{BuildInfo: buildInfo},
Package: &database.Package{
PURL: "pkg:npm/lodash",
Ecosystem: "npm",
Name: "lodash",
},
Version: &database.Version{
PURL: "pkg:npm/lodash@9.9.9",
PackagePURL: "pkg:npm/lodash",
},
},
shadow: "9.9.9",
},
{
name: "browse source page",
page: "browse_source",
data: BrowseSourceData{
Layout: Layout{BuildInfo: buildInfo},
Ecosystem: "npm",
PackageName: "lodash",
Version: "9.9.9",
},
shadow: "9.9.9",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
if err := templates.Render(w, tt.page, tt.data); err != nil {
t.Fatalf("Render(%q) failed: %v", tt.page, err)
}
body := w.Body.String()
if !strings.Contains(body, wantFooter) {
t.Errorf("footer missing build info %q", wantFooter)
}
if strings.Contains(body, "proxy "+tt.shadow) {
t.Errorf("footer used page Version %q instead of BuildInfo", tt.shadow)
}
})
}
}
func TestRenderOmitsCanonicalWhenUIBaseURLUnset(t *testing.T) {
templates := &Templates{}
w := httptest.NewRecorder()
if err := templates.Render(w, "dashboard", DashboardData{}); err != nil {
t.Fatalf("Render failed: %v", err)
}
body := w.Body.String()
if strings.Contains(body, `rel="canonical"`) {
t.Error("canonical tag should be omitted when UIBaseURL is empty")
}
if strings.Contains(body, `property="og:url"`) {
t.Error("og:url tag should be omitted when UIBaseURL is empty")
}
}
func TestInstallPageBannerWhenUIDiffersFromBaseURL(t *testing.T) {
templates := &Templates{}
data := struct {
Layout
BaseURL string
Registries []RegistryConfig
}{
Layout: Layout{
UIBaseURL: "https://ui.example.com/ui",
CanonicalPath: "/ui/install",
},
BaseURL: "http://pkg-proxy:8080",
Registries: getRegistryConfigs("http://pkg-proxy:8080"),
}
w := httptest.NewRecorder()
if err := templates.Render(w, "install", data); err != nil {
t.Fatalf("Render failed: %v", err)
}
body := w.Body.String()
if !strings.Contains(body, "https://ui.example.com/ui") || !strings.Contains(body, "http://pkg-proxy:8080") {
t.Error("install banner should mention both UIBaseURL and BaseURL when they differ")
}
if !strings.Contains(body, "package managers should be configured") {
t.Error("install banner copy missing")
}
}
func TestInstallPageNoBannerWhenURLsMatch(t *testing.T) {
templates := &Templates{}
data := struct {
Layout
BaseURL string
Registries []RegistryConfig
}{
Layout: Layout{
UIBaseURL: "http://localhost:8080",
CanonicalPath: "/ui/install",
},
BaseURL: "http://localhost:8080",
Registries: getRegistryConfigs("http://localhost:8080"),
}
w := httptest.NewRecorder()
if err := templates.Render(w, "install", data); err != nil {
t.Fatalf("Render failed: %v", err)
}
body := w.Body.String()
if strings.Contains(body, "package managers should be configured") {
t.Error("install banner should be hidden when UIBaseURL == BaseURL")
}
}
func TestTemplatesLazyLoading(t *testing.T) {
templates := &Templates{}
if templates.pages != nil {
t.Fatal("expected pages to be nil before first Render call")
}
w := httptest.NewRecorder()
_ = templates.Render(w, "dashboard", DashboardData{})
if templates.pages == nil {
t.Fatal("expected pages to be populated after first Render call")
}
}
func TestTemplatesRenderUnknownPage(t *testing.T) {
templates := &Templates{}
w := httptest.NewRecorder()
err := templates.Render(w, "nonexistent_page", nil)
if err == nil {
t.Error("expected error for unknown page")
}
}
func TestInstallPage(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
req := httptest.NewRequest("GET", "/ui/install", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
body := w.Body.String()
// Should contain instructions for all registries
registries := []string{"npm", "Cargo", "RubyGems", "Go Modules", "PyPI", "Maven", "Gradle Build Cache", "NuGet", "Composer", "Conan", "Conda", "CRAN"}
for _, reg := range registries {
if !strings.Contains(body, reg) {
t.Errorf("install page should contain %s instructions", reg)
}
}
}
func TestPackageShowPage(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Create a package with license and version
pkg := &database.Package{
PURL: "pkg:npm/test-show",
Ecosystem: "npm",
Name: "test-show",
LatestVersion: sql.NullString{String: "2.0.0", Valid: true},
License: sql.NullString{String: "MIT", Valid: true},
}
if err := ts.db.UpsertPackage(pkg); err != nil {
t.Fatalf("failed to upsert package: %v", err)
}
ver := &database.Version{PURL: "pkg:npm/test-show@2.0.0", PackagePURL: pkg.PURL}
if err := ts.db.UpsertVersion(ver); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
req := httptest.NewRequest("GET", "/ui/package/npm/test-show", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
body := w.Body.String()
if !strings.Contains(body, "test-show") {
t.Error("page should contain package name")
}
if !strings.Contains(body, "2.0.0") {
t.Error("page should contain version")
}
if !strings.Contains(body, "MIT") {
t.Error("page should contain license")
}
}
func TestPackageShowPage_NotFound(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
func TestVersionShowPage_NotFound(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent/1.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
func TestSearchPage_EmptyQuery(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
req := httptest.NewRequest("GET", "/ui/search", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
// Empty query should redirect to homepage
if w.Code != http.StatusSeeOther {
t.Errorf("expected redirect (303), got %d", w.Code)
}
}
func TestSearchPage_WithQuery(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
req := httptest.NewRequest("GET", "/ui/search?q=test", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "test") {
t.Error("search page should contain the query")
}
}
func TestSearchPage_Pagination(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Page 0 or negative should default to page 1
req := httptest.NewRequest("GET", "/ui/search?q=test&page=0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
// Non-numeric page should default to page 1
req = httptest.NewRequest("GET", "/ui/search?q=test&page=abc", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}
func TestSearchPage_EcosystemFilter(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
req := httptest.NewRequest("GET", "/ui/search?q=test&ecosystem=npm", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}
func TestEcosystemBadgeLabel(t *testing.T) {
tests := []struct {
ecosystem string
want string
}{
{"oci", "container"},
{"deb", "debian"},
{"npm", "npm"},
{"cargo", "cargo"},
}
for _, tt := range tests {
got := ecosystemBadgeLabel(tt.ecosystem)
if got != tt.want {
t.Errorf("ecosystemBadgeLabel(%q) = %q, want %q", tt.ecosystem, got, tt.want)
}
}
}
func TestOCIRegistryInstructionsDockerPull(t *testing.T) {
registries := getRegistryConfigs("http://package-proxy:8080")
for _, registry := range registries {
if registry.ID != "oci" {
continue
}
want := "docker pull package-proxy:8080/library/nginx:latest"
if !strings.Contains(string(registry.Instructions), want) {
t.Errorf("OCI instructions = %q, want substring %q", registry.Instructions, want)
}
return
}
t.Fatal("OCI registry instructions not found")
}
func TestSwiftRegistryInstructionsAllowLocalHTTP(t *testing.T) {
registries := getRegistryConfigs("http://localhost:8080")
for _, registry := range registries {
if registry.ID != "swift" {
continue
}
if !strings.Contains(string(registry.Instructions), "--allow-insecure-http") {
t.Error("Swift HTTP instructions do not allow the insecure local registry")
}
return
}
t.Fatal("Swift registry instructions not found")
}
func TestEcosystemBadgeClasses(t *testing.T) {
// Every supported ecosystem should return a non-empty class string
ecosystems := supportedEcosystems()
for _, eco := range ecosystems {
classes := ecosystemBadgeClasses(eco)
if classes == "" {
t.Errorf("ecosystemBadgeClasses(%q) returned empty string", eco)
}
if !strings.Contains(classes, "inline-flex") {
t.Errorf("ecosystemBadgeClasses(%q) missing base classes", eco)
}
}
// Unknown ecosystem should still return classes
classes := ecosystemBadgeClasses("unknown")
if !strings.Contains(classes, "gray") {
t.Error("unknown ecosystem should use gray classes")
}
}
func TestBuildEcosystemFilters(t *testing.T) {
filters := buildEcosystemFilters(map[string]int64{
"npm": 3,
"cargo": 2,
"empty": 0,
})
if len(filters) != 2 {
t.Fatalf("expected 2 filters, got %d", len(filters))
}
if filters[0].Ecosystem != "cargo" || filters[0].Count != 2 {
t.Errorf("first filter = %#v, want cargo with count 2", filters[0])
}
if filters[1].Ecosystem != "npm" || filters[1].Count != 3 {
t.Errorf("second filter = %#v, want npm with count 3", filters[1])
}
extra := buildEcosystemFilters(map[string]int64{"custom": 1})
if len(extra) != 1 || extra[0].Ecosystem != "custom" {
t.Errorf("unexpected extra filters: %#v", extra)
}
}
func TestEcosystemPillClasses(t *testing.T) {
classes := ecosystemPillClasses("npm")
if !strings.Contains(classes, "rounded-full") {
t.Error("pill classes should use rounded-full")
}
if !strings.Contains(classes, "bg-red-100") {
t.Error("npm pill should use npm colors")
}
}
func TestFormatSize(t *testing.T) {
tests := []struct {
bytes int64
want string
}{
{0, "0 B"},
{100, "100 B"},
{1024, "1.0 KB"},
{1536, "1.5 KB"},
{1048576, "1.0 MB"},
{1073741824, "1.0 GB"},
}
for _, tt := range tests {
got := formatSize(tt.bytes)
if got != tt.want {
t.Errorf("formatSize(%d) = %q, want %q", tt.bytes, got, tt.want)
}
}
}
func TestCategorizeLicense(t *testing.T) {
tests := []struct {
license sql.NullString
want string
}{
{sql.NullString{Valid: false}, "unknown"},
{sql.NullString{String: "", Valid: true}, "unknown"},
{sql.NullString{String: "MIT", Valid: true}, "permissive"},
{sql.NullString{String: "GPL-3.0", Valid: true}, "copyleft"},
}
for _, tt := range tests {
got := categorizeLicense(tt.license)
if got != tt.want {
t.Errorf("categorizeLicense(%v) = %q, want %q", tt.license, got, tt.want)
}
}
}
func BenchmarkTemplatesParse(b *testing.B) {
for b.Loop() {
t := &Templates{}
if err := t.load(); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkServerCreate(b *testing.B) {
for b.Loop() {
_ = &Server{
templates: &Templates{},
}
}
}
func BenchmarkFirstRender(b *testing.B) {
for b.Loop() {
t := &Templates{}
w := httptest.NewRecorder()
if err := t.Render(w, "dashboard", DashboardData{}); err != nil {
b.Fatal(err)
}
}
}
// thirdPartyAsset matches a script or stylesheet loaded from another origin.
var thirdPartyAsset = regexp.MustCompile(`<(?:script[^>]*\ssrc|link[^>]*\shref)="(?:https?:)?//[^"]*"`)
// Pages run on the proxy origin, so a script or stylesheet from elsewhere
// would run with the proxy's privileges and break offline deployments.
// Vendor it under static/vendor instead.
func TestTemplatesLoadNoThirdPartyAssets(t *testing.T) {
err := fs.WalkDir(templatesFS, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
content, err := templatesFS.ReadFile(path)
if err != nil {
return err
}
for _, match := range thirdPartyAsset.FindAllString(string(content), -1) {
t.Errorf("%s loads a third-party asset: %s", path, match)
}
return nil
})
if err != nil {
t.Fatal(err)
}
}