mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-06-02 00:38:16 -04:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea4d47b13 |
20 changed files with 526 additions and 490 deletions
18
README.md
18
README.md
|
|
@ -819,16 +819,16 @@ Response:
|
|||
|
||||
## Web Interface
|
||||
|
||||
The proxy serves a web UI at the root URL. No separate frontend build is needed -- templates and assets are embedded in the binary.
|
||||
The proxy serves a web UI under `/ui`. No separate frontend build is needed -- templates and assets are embedded in the binary. `GET /` redirects to `/ui/`. The UI is mounted under its own prefix so a reverse proxy can apply different access rules to it than to the package endpoints (for example, requiring auth for `PathPrefix(/ui)` while leaving `/npm`, `/pypi` etc. open to build machines).
|
||||
|
||||
- **Dashboard** (`/`) -- cache stats, popular packages, recently cached artifacts, and vulnerability overview.
|
||||
- **Install guide** (`/install`) -- per-ecosystem configuration instructions, so you don't have to look them up here.
|
||||
- **Package browser** (`/packages`) -- browse all cached packages with filtering by ecosystem and sorting by hits, size, name, or vulnerability count.
|
||||
- **Search** (`/search?q=...`) -- search cached packages by name.
|
||||
- **Package detail** (`/package/{ecosystem}/{name}`) -- metadata, license, vulnerabilities, and version list for a package. You can select two versions to compare.
|
||||
- **Version detail** (`/package/{ecosystem}/{name}/{version}`) -- per-version metadata, integrity hash, artifact cache status, and hit counts.
|
||||
- **Source browser** (`/package/{ecosystem}/{name}/{version}/browse`) -- browse files inside cached archives with syntax highlighting for text files and image previews.
|
||||
- **Version diff** (`/package/{ecosystem}/{name}/compare/{v1}...{v2}`) -- side-by-side diff of two cached versions showing added, removed, and changed files.
|
||||
- **Dashboard** (`/ui/`) -- cache stats, popular packages, recently cached artifacts, and vulnerability overview.
|
||||
- **Install guide** (`/ui/install`) -- per-ecosystem configuration instructions, so you don't have to look them up here.
|
||||
- **Package browser** (`/ui/packages`) -- browse all cached packages with filtering by ecosystem and sorting by hits, size, name, or vulnerability count.
|
||||
- **Search** (`/ui/search?q=...`) -- search cached packages by name.
|
||||
- **Package detail** (`/ui/package/{ecosystem}/{name}`) -- metadata, license, vulnerabilities, and version list for a package. You can select two versions to compare.
|
||||
- **Version detail** (`/ui/package/{ecosystem}/{name}/{version}`) -- per-version metadata, integrity hash, artifact cache status, and hit counts.
|
||||
- **Source browser** (`/ui/package/{ecosystem}/{name}/{version}/browse`) -- browse files inside cached archives with syntax highlighting for text files and image previews.
|
||||
- **Version diff** (`/ui/package/{ecosystem}/{name}/compare/{v1}...{v2}`) -- side-by-side diff of two cached versions showing added, removed, and changed files.
|
||||
|
||||
## Monitoring
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ The proxy is a caching HTTP server that sits between package manager clients and
|
|||
│ │ /cargo/* -> CargoHandler /stats -> statsHandler │ │
|
||||
│ │ /gem/* -> GemHandler /metrics -> prometheus │ │
|
||||
│ │ ...17 ecosystems /api/* -> APIHandler │ │
|
||||
│ │ / -> Web UI │ │
|
||||
│ │ /ui/* -> Web UI │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
|
|
@ -274,7 +274,7 @@ HTTP server setup, web UI, and API handlers.
|
|||
- Creates and wires together all components
|
||||
- Mounts protocol handlers at ecosystem-specific paths
|
||||
- Middleware: request ID, real IP, logging, panic recovery, active request tracking
|
||||
- Web UI: dashboard, package browser, source browser, version comparison
|
||||
- Web UI under `/ui`: dashboard, package browser, source browser, version comparison
|
||||
- Templates are embedded in the binary via `//go:embed`
|
||||
- Enrichment API for package metadata, vulnerability scanning, and outdated detection
|
||||
- Health, stats, and Prometheus metrics endpoints. `/health` runs an active write → size-check → read → verify → delete probe against the storage backend and returns a structured JSON response (`HealthResponse`) with `"ok"` / `"error"` status per subsystem. Probe results are cached (default 30 s, configurable via `health.storage_probe_interval`) to avoid overwhelming remote backends.
|
||||
|
|
|
|||
|
|
@ -15,135 +15,6 @@ const docTemplate = `{
|
|||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/api/browse/{ecosystem}/{name}/{version}": {
|
||||
"get": {
|
||||
"description": "Lists files from the first cached artifact for a package version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "List files inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Directory path inside the archive",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.BrowseListResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/browse/{ecosystem}/{name}/{version}/file/{filepath}": {
|
||||
"get": {
|
||||
"description": "Streams a single file from the cached artifact. The file path may contain slashes.",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Fetch a file inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "File path inside the archive",
|
||||
"name": "filepath",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/bulk": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
|
|
@ -189,69 +60,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}": {
|
||||
"get": {
|
||||
"description": "Returns a structured diff for two cached versions.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Compare two cached versions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "From version",
|
||||
"name": "fromVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "To version",
|
||||
"name": "toVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/outdated": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
|
|
@ -445,6 +253,198 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ui/api/browse/{ecosystem}/{name}/{version}": {
|
||||
"get": {
|
||||
"description": "Lists files from the first cached artifact for a package version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "List files inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Directory path inside the archive",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.BrowseListResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ui/api/browse/{ecosystem}/{name}/{version}/file/{filepath}": {
|
||||
"get": {
|
||||
"description": "Streams a single file from the cached artifact. The file path may contain slashes.",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Fetch a file inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "File path inside the archive",
|
||||
"name": "filepath",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}": {
|
||||
"get": {
|
||||
"description": "Returns a structured diff for two cached versions.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Compare two cached versions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "From version",
|
||||
"name": "fromVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "To version",
|
||||
"name": "toVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
|
|
|||
|
|
@ -8,135 +8,6 @@
|
|||
},
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/api/browse/{ecosystem}/{name}/{version}": {
|
||||
"get": {
|
||||
"description": "Lists files from the first cached artifact for a package version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "List files inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Directory path inside the archive",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.BrowseListResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/browse/{ecosystem}/{name}/{version}/file/{filepath}": {
|
||||
"get": {
|
||||
"description": "Streams a single file from the cached artifact. The file path may contain slashes.",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Fetch a file inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "File path inside the archive",
|
||||
"name": "filepath",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/bulk": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
|
|
@ -182,69 +53,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}": {
|
||||
"get": {
|
||||
"description": "Returns a structured diff for two cached versions.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Compare two cached versions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "From version",
|
||||
"name": "fromVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "To version",
|
||||
"name": "toVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/outdated": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
|
|
@ -438,6 +246,198 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ui/api/browse/{ecosystem}/{name}/{version}": {
|
||||
"get": {
|
||||
"description": "Lists files from the first cached artifact for a package version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "List files inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Directory path inside the archive",
|
||||
"name": "path",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.BrowseListResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ui/api/browse/{ecosystem}/{name}/{version}/file/{filepath}": {
|
||||
"get": {
|
||||
"description": "Streams a single file from the cached artifact. The file path may contain slashes.",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Fetch a file inside a cached artifact",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Version",
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "File path inside the archive",
|
||||
"name": "filepath",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}": {
|
||||
"get": {
|
||||
"description": "Returns a structured diff for two cached versions.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"browse"
|
||||
],
|
||||
"summary": "Compare two cached versions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Ecosystem",
|
||||
"name": "ecosystem",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Package name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "From version",
|
||||
"name": "fromVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "To version",
|
||||
"name": "toVersion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ type BrowseFileInfo struct {
|
|||
// @Success 200 {object} BrowseListResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/browse/{ecosystem}/{name}/{version} [get]
|
||||
// @Router /ui/api/browse/{ecosystem}/{name}/{version} [get]
|
||||
// handleBrowsePath dispatches /api/browse/{ecosystem}/* to the appropriate browse handler.
|
||||
// It resolves namespaced package names by consulting the database.
|
||||
//
|
||||
|
|
@ -296,7 +296,7 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n
|
|||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/browse/{ecosystem}/{name}/{version}/file/{filepath} [get]
|
||||
// @Router /ui/api/browse/{ecosystem}/{name}/{version}/file/{filepath} [get]
|
||||
func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, name, version, filePath string) {
|
||||
if filePath == "" {
|
||||
badRequest(w, "file path required")
|
||||
|
|
@ -498,7 +498,7 @@ type BrowseSourceData struct {
|
|||
// @Success 200 {object} map[string]any
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion} [get]
|
||||
// @Router /ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion} [get]
|
||||
func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem, name, fromVersion, toVersion string) {
|
||||
// Get artifacts for both versions
|
||||
fromPURL := purl.MakePURLString(ecosystem, name, fromVersion)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func TestHandleBrowseList(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test listing root directory
|
||||
req := httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ func TestHandleBrowseList(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test listing subdirectory
|
||||
req = httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0?path=lib", nil)
|
||||
req = httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0?path=lib", nil)
|
||||
w = httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ func TestHandleBrowseFile(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test fetching a file
|
||||
req := httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0/file/README.md", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/README.md", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ func TestHandleBrowseFile(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test fetching non-existent file
|
||||
req = httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
|
||||
req = httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
|
||||
w = httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ func TestBrowseNonCachedArtifact(t *testing.T) {
|
|||
}
|
||||
|
||||
// Try to browse
|
||||
req := httptest.NewRequest("GET", "/api/browse/npm/not-cached/1.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/api/browse/npm/not-cached/1.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -368,7 +368,7 @@ func TestHandleBrowseSourcePage(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test the browse source page loads
|
||||
req := httptest.NewRequest("GET", "/package/npm/test-browse/1.0.0/browse", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/test-browse/1.0.0/browse", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -501,7 +501,7 @@ func TestHandleCompareDiff(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test the compare endpoint
|
||||
req := httptest.NewRequest("GET", "/api/compare/npm/test-compare/1.0.0/2.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/api/compare/npm/test-compare/1.0.0/2.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -572,7 +572,7 @@ func TestHandleComparePage(t *testing.T) {
|
|||
defer ts.close()
|
||||
|
||||
// Test valid format with ... separator
|
||||
req := httptest.NewRequest("GET", "/package/npm/test/compare/1.0.0...2.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/test/compare/1.0.0...2.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -591,7 +591,7 @@ func TestHandleComparePage(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test invalid format (missing separator)
|
||||
req = httptest.NewRequest("GET", "/package/npm/test/compare/invalid", nil)
|
||||
req = httptest.NewRequest("GET", "/ui/package/npm/test/compare/invalid", nil)
|
||||
w = httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -600,7 +600,7 @@ func TestHandleComparePage(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test with only one dot (should fail)
|
||||
req = httptest.NewRequest("GET", "/package/npm/test/compare/1.0.0.2.0.0", nil)
|
||||
req = httptest.NewRequest("GET", "/ui/package/npm/test/compare/1.0.0.2.0.0", nil)
|
||||
w = httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,11 +21,20 @@
|
|||
// - /rpm/* - RPM/Yum repository protocol
|
||||
//
|
||||
// Additional endpoints:
|
||||
// - /health - Health check endpoint
|
||||
// - /stats - Cache statistics (JSON)
|
||||
// - /health - Health check endpoint
|
||||
// - /stats - Cache statistics (JSON)
|
||||
// - /openapi.json - OpenAPI spec (JSON)
|
||||
// - /packages - List all cached packages (HTML)
|
||||
// - /search - Search packages (HTML)
|
||||
// - /metrics - Prometheus metrics
|
||||
//
|
||||
// Web UI (HTML), mounted under /ui so reverse proxies can gate it
|
||||
// separately from the package endpoints:
|
||||
// - /ui/ - Dashboard
|
||||
// - /ui/install - Client configuration guide
|
||||
// - /ui/packages - List all cached packages
|
||||
// - /ui/search - Search packages
|
||||
// - /ui/package/... - Package and version detail pages
|
||||
// - /ui/api/browse/... - Archive browsing (used by the UI)
|
||||
// - /ui/api/compare/... - Archive diffing (used by the UI)
|
||||
//
|
||||
// API endpoints for enrichment data:
|
||||
// - GET /api/package/{ecosystem}/{name} - Package metadata
|
||||
|
|
@ -229,19 +238,29 @@ func (s *Server) Start() error {
|
|||
r.Mount("/debian", http.StripPrefix("/debian", debianHandler.Routes()))
|
||||
r.Mount("/rpm", http.StripPrefix("/rpm", rpmHandler.Routes()))
|
||||
|
||||
// Health, stats, and static endpoints
|
||||
// Health, stats, and metrics endpoints
|
||||
r.Get("/health", s.handleHealth)
|
||||
r.Get("/stats", s.handleStats)
|
||||
r.Get("/openapi.json", s.handleOpenAPIJSON)
|
||||
r.Get("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||
metrics.Handler().ServeHTTP(w, r)
|
||||
})
|
||||
r.Mount("/static", http.StripPrefix("/static/", staticHandler()))
|
||||
r.Get("/", s.handleRoot)
|
||||
r.Get("/install", s.handleInstall)
|
||||
r.Get("/search", s.handleSearch)
|
||||
r.Get("/packages", s.handlePackagesList)
|
||||
r.Get("/package/{ecosystem}/*", s.handlePackagePath)
|
||||
|
||||
// Web UI. Mounted under /ui so a reverse proxy can apply different
|
||||
// access rules to it than to the package endpoints above (#123).
|
||||
r.Route("/ui", func(ui chi.Router) {
|
||||
ui.Mount("/static", http.StripPrefix("/ui/static/", staticHandler()))
|
||||
ui.Get("/", s.handleRoot)
|
||||
ui.Get("/install", s.handleInstall)
|
||||
ui.Get("/search", s.handleSearch)
|
||||
ui.Get("/packages", s.handlePackagesList)
|
||||
ui.Get("/package/{ecosystem}/*", s.handlePackagePath)
|
||||
ui.Get("/api/browse/{ecosystem}/*", s.handleBrowsePath)
|
||||
ui.Get("/api/compare/{ecosystem}/*", s.handleComparePath)
|
||||
})
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/ui/", http.StatusFound)
|
||||
})
|
||||
|
||||
// API endpoints for enrichment data
|
||||
enrichSvc := enrichment.New(s.logger)
|
||||
|
|
@ -254,10 +273,6 @@ func (s *Server) Start() error {
|
|||
r.Get("/api/search", apiHandler.HandleSearch)
|
||||
r.Get("/api/packages", apiHandler.HandlePackagesList)
|
||||
|
||||
// Archive browsing and comparison endpoints also use wildcard for namespaced packages
|
||||
r.Get("/api/browse/{ecosystem}/*", s.handleBrowsePath)
|
||||
r.Get("/api/compare/{ecosystem}/*", s.handleComparePath)
|
||||
|
||||
// Start background context (used by mirror jobs and cleanup)
|
||||
bgCtx, bgCancel := context.WithCancel(context.Background())
|
||||
s.cancel = bgCancel
|
||||
|
|
@ -488,7 +503,7 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
|||
ecosystem := r.URL.Query().Get("ecosystem")
|
||||
|
||||
if query == "" {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/ui/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,14 +101,19 @@ func newTestServer(t *testing.T) *testServer {
|
|||
r.Get("/health", s.handleHealth)
|
||||
r.Get("/stats", s.handleStats)
|
||||
r.Get("/openapi.json", s.handleOpenAPIJSON)
|
||||
r.Mount("/static", http.StripPrefix("/static/", staticHandler()))
|
||||
r.Get("/search", s.handleSearch)
|
||||
r.Get("/package/{ecosystem}/*", s.handlePackagePath)
|
||||
r.Get("/api/browse/{ecosystem}/*", s.handleBrowsePath)
|
||||
r.Get("/api/compare/{ecosystem}/*", s.handleComparePath)
|
||||
r.Get("/", s.handleRoot)
|
||||
r.Get("/install", s.handleInstall)
|
||||
r.Get("/packages", s.handlePackagesList)
|
||||
r.Route("/ui", func(ui chi.Router) {
|
||||
ui.Mount("/static", http.StripPrefix("/ui/static/", staticHandler()))
|
||||
ui.Get("/", s.handleRoot)
|
||||
ui.Get("/install", s.handleInstall)
|
||||
ui.Get("/search", s.handleSearch)
|
||||
ui.Get("/packages", s.handlePackagesList)
|
||||
ui.Get("/package/{ecosystem}/*", s.handlePackagePath)
|
||||
ui.Get("/api/browse/{ecosystem}/*", s.handleBrowsePath)
|
||||
ui.Get("/api/compare/{ecosystem}/*", s.handleComparePath)
|
||||
})
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/ui/", http.StatusFound)
|
||||
})
|
||||
|
||||
return &testServer{
|
||||
handler: r,
|
||||
|
|
@ -274,7 +279,7 @@ func TestDashboard(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -445,8 +450,8 @@ func TestStaticFiles(t *testing.T) {
|
|||
path string
|
||||
contentTypes []string
|
||||
}{
|
||||
{"/static/tailwind.js", []string{"text/javascript", "application/javascript"}},
|
||||
{"/static/style.css", []string{"text/css"}},
|
||||
{"/ui/static/tailwind.js", []string{"text/javascript", "application/javascript"}},
|
||||
{"/ui/static/style.css", []string{"text/css"}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
|
@ -497,7 +502,7 @@ func TestCategorizeLicenseCSS(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDashboardWithEnrichmentStats(t *testing.T) {
|
||||
func TestRootRedirectsToUI(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
|
|
@ -505,6 +510,22 @@ func TestDashboardWithEnrichmentStats(t *testing.T) {
|
|||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusFound {
|
||||
t.Errorf("expected status 302, got %d", w.Code)
|
||||
}
|
||||
if loc := w.Header().Get("Location"); loc != "/ui/" {
|
||||
t.Errorf("expected redirect to /ui/, got %q", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardWithEnrichmentStats(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/ui/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
|
@ -512,7 +533,7 @@ func TestDashboardWithEnrichmentStats(t *testing.T) {
|
|||
body := w.Body.String()
|
||||
|
||||
// Dashboard should link to Tailwind JS
|
||||
if !strings.Contains(body, "/static/tailwind.js") {
|
||||
if !strings.Contains(body, "/ui/static/tailwind.js") {
|
||||
t.Error("dashboard should link to Tailwind JS")
|
||||
}
|
||||
|
||||
|
|
@ -553,7 +574,7 @@ func TestVersionShowWithHitCount(t *testing.T) {
|
|||
t.Fatalf("failed to upsert artifact: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/test/1.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/test/1.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
|
@ -605,7 +626,7 @@ func TestSearchWithNullValues(t *testing.T) {
|
|||
t.Fatalf("failed to upsert artifact: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/search?q=test", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
|
@ -697,7 +718,7 @@ func TestSearchRedirectsWhenEmpty(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -706,8 +727,8 @@ func TestSearchRedirectsWhenEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
loc := w.Header().Get("Location")
|
||||
if loc != "/" {
|
||||
t.Errorf("expected redirect to /, got %q", loc)
|
||||
if loc != "/ui/" {
|
||||
t.Errorf("expected redirect to /ui/, got %q", loc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -715,7 +736,7 @@ func TestPackageShowPage_NotFoundServer(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/nonexistent-srv", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent-srv", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -728,7 +749,7 @@ func TestVersionShowPage_NotFoundServer(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/nonexistent-srv/1.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent-srv/1.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -759,7 +780,7 @@ func TestPackageShowPage_WithLicense(t *testing.T) {
|
|||
t.Fatalf("failed to upsert version: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/show-test-lic", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/show-test-lic", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -801,8 +822,8 @@ func TestComposerNamespacedPackageRoutes(t *testing.T) {
|
|||
url string
|
||||
want string
|
||||
}{
|
||||
{"package show", "/package/composer/monolog/monolog", "monolog/monolog"},
|
||||
{"version show", "/package/composer/symfony/console/6.0.0", "symfony/console"},
|
||||
{"package show", "/ui/package/composer/monolog/monolog", "monolog/monolog"},
|
||||
{"version show", "/ui/package/composer/symfony/console/6.0.0", "symfony/console"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -859,11 +880,11 @@ func TestNamespacedPackageRoutes(t *testing.T) {
|
|||
url string
|
||||
want int
|
||||
}{
|
||||
{"npm scoped package show", "/package/npm/@babel/core", http.StatusOK},
|
||||
{"golang module show", "/package/golang/github.com/stretchr/testify", http.StatusOK},
|
||||
{"oci image show", "/package/oci/library/nginx", http.StatusOK},
|
||||
{"conda package show", "/package/conda/conda-forge/numpy", http.StatusOK},
|
||||
{"conan package show", "/package/conan/zlib/1.2.13@demo/stable", http.StatusOK},
|
||||
{"npm scoped package show", "/ui/package/npm/@babel/core", http.StatusOK},
|
||||
{"golang module show", "/ui/package/golang/github.com/stretchr/testify", http.StatusOK},
|
||||
{"oci image show", "/ui/package/oci/library/nginx", http.StatusOK},
|
||||
{"conda package show", "/ui/package/conda/conda-forge/numpy", http.StatusOK},
|
||||
{"conan package show", "/ui/package/conan/zlib/1.2.13@demo/stable", http.StatusOK},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -886,7 +907,7 @@ func TestSearchPage_WithSeededResults(t *testing.T) {
|
|||
|
||||
seedTestPackage(t, ts.db, "searchable-pkg")
|
||||
|
||||
req := httptest.NewRequest("GET", "/search?q=searchable", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=searchable", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -934,7 +955,7 @@ func TestSearchPage_PaginationMultiPage(t *testing.T) {
|
|||
}
|
||||
|
||||
// First page
|
||||
req := httptest.NewRequest("GET", "/search?q=page-test", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=page-test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -948,7 +969,7 @@ func TestSearchPage_PaginationMultiPage(t *testing.T) {
|
|||
}
|
||||
|
||||
// Second page
|
||||
req = httptest.NewRequest("GET", "/search?q=page-test&page=2", nil)
|
||||
req = httptest.NewRequest("GET", "/ui/search?q=page-test&page=2", nil)
|
||||
w = httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -1014,7 +1035,7 @@ func TestSearchPage_EcosystemFilterWithSeededData(t *testing.T) {
|
|||
}
|
||||
|
||||
// Search with ecosystem filter for npm only
|
||||
req := httptest.NewRequest("GET", "/search?q=eco-filter&ecosystem=npm", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=eco-filter&ecosystem=npm", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -1037,7 +1058,7 @@ func TestHandlePackagesListPage(t *testing.T) {
|
|||
|
||||
seedTestPackage(t, ts.db, "list-test")
|
||||
|
||||
req := httptest.NewRequest("GET", "/packages", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/packages", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}git-pkgs proxy{{end}}</title>
|
||||
<script src="/static/tailwind.js"></script>
|
||||
<script src="/ui/static/tailwind.js"></script>
|
||||
<script>
|
||||
tailwind.config = { darkMode: 'class' }
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">Resources</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/install" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Configuration Guide</a></li>
|
||||
<li><a href="/ui/install" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Configuration Guide</a></li>
|
||||
<li><a href="/health" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Health Check</a></li>
|
||||
<li><a href="/stats" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">API Stats</a></li>
|
||||
<li><a href="/openapi.json" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">OpenAPI Spec</a></li>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
<svg class="w-8 h-8 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<a href="/" class="text-xl font-semibold hover:text-gray-700 dark:hover:text-gray-300">git-pkgs proxy</a>
|
||||
<a href="/ui/" class="text-xl font-semibold hover:text-gray-700 dark:hover:text-gray-300">git-pkgs proxy</a>
|
||||
</div>
|
||||
<div class="flex-1 max-w-md mx-8">
|
||||
<form action="/search" method="get" class="relative">
|
||||
<form action="/ui/search" method="get" class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<nav class="flex items-center gap-6">
|
||||
<a href="/install" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Install</a>
|
||||
<a href="/ui/install" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Install</a>
|
||||
<a href="/health" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Health</a>
|
||||
<a href="/stats" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">API</a>
|
||||
<button id="theme-toggle" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
{{define "content"}}
|
||||
<div class="mb-6">
|
||||
<nav class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
<a href="/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
||||
<a href="/ui/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
||||
<span class="mx-2">/</span>
|
||||
<a href="/package/{{.Ecosystem}}/{{.PackageName}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.PackageName}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.PackageName}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.PackageName}}</a>
|
||||
<span class="mx-2">/</span>
|
||||
<a href="/package/{{.Ecosystem}}/{{.PackageName}}/{{.Version}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.Version}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.PackageName}}/{{.Version}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.Version}}</a>
|
||||
<span class="mx-2">/</span>
|
||||
<span>Browse Source</span>
|
||||
</nav>
|
||||
|
|
@ -65,7 +65,7 @@ function escapeHTML(str) {
|
|||
// Load file tree for a directory
|
||||
async function loadFileTree(path = '') {
|
||||
try {
|
||||
const url = `/api/browse/${ecosystem}/${packageName}/${version}?path=${encodeURIComponent(path)}`;
|
||||
const url = `/ui/api/browse/${ecosystem}/${packageName}/${version}?path=${encodeURIComponent(path)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load directory');
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ function renderFileTree(files, basePath) {
|
|||
// Load and display file content
|
||||
async function loadFile(path) {
|
||||
try {
|
||||
const url = `/api/browse/${ecosystem}/${packageName}/${version}/file/${path}`;
|
||||
const url = `/ui/api/browse/${ecosystem}/${packageName}/${version}/file/${path}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load file');
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
{{define "content"}}
|
||||
<div class="mb-6">
|
||||
<nav class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
<a href="/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
||||
<a href="/ui/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
||||
<span class="mx-2">/</span>
|
||||
<a href="/package/{{.Ecosystem}}/{{.PackageName}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.PackageName}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.PackageName}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.PackageName}}</a>
|
||||
<span class="mx-2">/</span>
|
||||
<span>Compare Versions</span>
|
||||
</nav>
|
||||
|
|
@ -79,7 +79,7 @@ const toVersion = '{{.ToVersion}}';
|
|||
|
||||
async function loadDiff() {
|
||||
try {
|
||||
const url = `/api/compare/${ecosystem}/${packageName}/${fromVersion}/${toVersion}`;
|
||||
const url = `/ui/api/compare/${ecosystem}/${packageName}/${fromVersion}/${toVersion}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
{{template "ecosystem_badge" .Ecosystem}}
|
||||
<a href="/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
{{if .License}}<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-{{if eq .LicenseCategory "permissive"}}green{{else if eq .LicenseCategory "copyleft"}}pink{{else}}gray{{end}}-100 text-{{if eq .LicenseCategory "permissive"}}green{{else if eq .LicenseCategory "copyleft"}}pink{{else}}gray{{end}}-700 dark:bg-{{if eq .LicenseCategory "permissive"}}green{{else if eq .LicenseCategory "copyleft"}}pink{{else}}gray{{end}}-900 dark:text-{{if eq .LicenseCategory "permissive"}}green{{else if eq .LicenseCategory "copyleft"}}pink{{else}}gray{{end}}-300">{{.License}}</span>{{end}}
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="px-6 py-3 text-center border-t border-gray-200 dark:border-gray-800">
|
||||
<a href="/packages?sort=hits" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">See all packages →</a>
|
||||
<a href="/ui/packages?sort=hits" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">See all packages →</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">No packages cached yet</div>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
{{template "ecosystem_badge" .Ecosystem}}
|
||||
<a href="/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
<span class="text-gray-500 dark:text-gray-400">@{{.Version}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="px-6 py-3 text-center border-t border-gray-200 dark:border-gray-800">
|
||||
<a href="/packages?sort=cached_at" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">See all packages →</a>
|
||||
<a href="/ui/packages?sort=cached_at" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">See all packages →</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">No packages cached yet</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<h2 class="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-100">Need Help?</h2>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
After configuring your package manager, install packages as usual. The proxy will automatically cache them.
|
||||
Check the <a href="/" class="underline hover:no-underline">dashboard</a> to see cached packages.
|
||||
Check the <a href="/ui/" class="underline hover:no-underline">dashboard</a> to see cached packages.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
<div class="px-6 py-3 flex items-center justify-between version-row">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" class="version-checkbox hidden" data-version="{{.Version}}" />
|
||||
<a href="/package/{{$.Package.Ecosystem}}/{{$.Package.Name}}/{{.Version}}" class="font-mono text-sm hover:text-blue-600 dark:hover:text-blue-400">{{.PURL}}</a>
|
||||
<a href="/ui/package/{{$.Package.Ecosystem}}/{{$.Package.Name}}/{{.Version}}" class="font-mono text-sm hover:text-blue-600 dark:hover:text-blue-400">{{.PURL}}</a>
|
||||
{{if .Yanked}}<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300">yanked</span>{{end}}
|
||||
</div>
|
||||
{{if .PublishedAt.Valid}}<span class="text-sm text-gray-500 dark:text-gray-400">{{.PublishedAt.Time.Format "2006-01-02"}}</span>{{end}}
|
||||
|
|
@ -123,7 +123,7 @@ document.addEventListener('change', function(e) {
|
|||
// Navigate to compare page
|
||||
const v1 = checked[0].dataset.version;
|
||||
const v2 = checked[1].dataset.version;
|
||||
window.location.href = `/package/${ecosystem}/${packageName}/compare/${v1}...${v2}`;
|
||||
window.location.href = `/ui/package/${ecosystem}/${packageName}/compare/${v1}...${v2}`;
|
||||
} else if (checked.length > 2) {
|
||||
// Uncheck the oldest selection
|
||||
checked[0].checked = false;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
{{template "ecosystem_badge" .Ecosystem}}
|
||||
<a href="/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
{{if .LatestVersion}}
|
||||
<span class="text-gray-500 dark:text-gray-400">@{{.LatestVersion}}</span>
|
||||
{{end}}
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
{{else}}
|
||||
<div class="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 p-12 text-center">
|
||||
<p class="text-gray-500 dark:text-gray-400">No cached packages found{{if .Ecosystem}} in {{.Ecosystem}}{{end}}</p>
|
||||
<a href="/" class="mt-4 inline-block text-sm text-blue-600 dark:text-blue-400 hover:underline">Return to dashboard</a>
|
||||
<a href="/ui/" class="mt-4 inline-block text-sm text-blue-600 dark:text-blue-400 hover:underline">Return to dashboard</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
{{template "ecosystem_badge" .Ecosystem}}
|
||||
<a href="/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
<a href="/ui/package/{{.Ecosystem}}/{{.Name}}" class="font-medium truncate hover:text-blue-600 dark:hover:text-blue-400">{{.Name}}</a>
|
||||
{{if .LatestVersion}}
|
||||
<span class="text-gray-500 dark:text-gray-400">@{{.LatestVersion}}</span>
|
||||
{{end}}
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
{{else}}
|
||||
<div class="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 p-12 text-center">
|
||||
<p class="text-gray-500 dark:text-gray-400">No packages found matching "{{.Query}}"</p>
|
||||
<a href="/" class="mt-4 inline-block text-sm text-blue-600 dark:text-blue-400 hover:underline">Return to dashboard</a>
|
||||
<a href="/ui/" class="mt-4 inline-block text-sm text-blue-600 dark:text-blue-400 hover:underline">Return to dashboard</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
{{define "content"}}
|
||||
<div class="mb-6">
|
||||
<nav class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
<a href="/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
||||
<a href="/ui/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
||||
<span class="mx-2">/</span>
|
||||
<a href="/package/{{.Package.Ecosystem}}/{{.Package.Name}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.Package.Name}}</a>
|
||||
<a href="/ui/package/{{.Package.Ecosystem}}/{{.Package.Name}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.Package.Name}}</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
{{template "ecosystem_badge" .Package.Ecosystem}}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
{{end}}
|
||||
{{if .HasCachedArtifact}}
|
||||
<div class="mt-4">
|
||||
<a href="/package/{{.Package.Ecosystem}}/{{.Package.Name}}/{{.Version.Version}}/browse"
|
||||
<a href="/ui/package/{{.Package.Ecosystem}}/{{.Package.Name}}/{{.Version.Version}}/browse"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ func TestInstallPage(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/install", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/install", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ func TestPackageShowPage(t *testing.T) {
|
|||
t.Fatalf("failed to upsert version: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/test-show", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/test-show", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ func TestPackageShowPage_NotFound(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/nonexistent", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ func TestVersionShowPage_NotFound(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/package/npm/nonexistent/1.0.0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent/1.0.0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ func TestSearchPage_EmptyQuery(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -285,7 +285,7 @@ func TestSearchPage_WithQuery(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/search?q=test", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -304,7 +304,7 @@ func TestSearchPage_Pagination(t *testing.T) {
|
|||
defer ts.close()
|
||||
|
||||
// Page 0 or negative should default to page 1
|
||||
req := httptest.NewRequest("GET", "/search?q=test&page=0", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=test&page=0", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -313,7 +313,7 @@ func TestSearchPage_Pagination(t *testing.T) {
|
|||
}
|
||||
|
||||
// Non-numeric page should default to page 1
|
||||
req = httptest.NewRequest("GET", "/search?q=test&page=abc", nil)
|
||||
req = httptest.NewRequest("GET", "/ui/search?q=test&page=abc", nil)
|
||||
w = httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ func TestSearchPage_EcosystemFilter(t *testing.T) {
|
|||
ts := newTestServer(t)
|
||||
defer ts.close()
|
||||
|
||||
req := httptest.NewRequest("GET", "/search?q=test&ecosystem=npm", nil)
|
||||
req := httptest.NewRequest("GET", "/ui/search?q=test&ecosystem=npm", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.handler.ServeHTTP(w, req)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue