Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-08-23 12:24:57 -04:00
pkg-proxy/internal/server/templates_test.go
Ching Wei Kang c1f09e7921
Show build information in web UI (#257)
* Show build information in web UI

Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com>

* Fix footer build info shadowed by page Version fields

Shared footer templates were reading .Version and .Commit, which resolve
to package data on VersionShowData and BrowseSourceData. Point the footer
at Layout.BuildInfo and cover both pages so the proxy version stays visible.

Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Layout build info field promotion

---------

Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 09:25:02 +01:00

607 lines
15 KiB
Go

package server
import (
"database/sql"
"net/http"
"net/http/httptest"
"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"},
},
}},
{"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,
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 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 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)
}
}
}