feat: complete 2.0.7.12 platform overhaul
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ymhut-box/server/unified-management/internal/adminassets"
|
||||
"ymhut-box/server/unified-management/internal/auth"
|
||||
"ymhut-box/server/unified-management/internal/config"
|
||||
"ymhut-box/server/unified-management/internal/db"
|
||||
@@ -36,6 +38,74 @@ import (
|
||||
"ymhut-box/server/unified-management/internal/sources"
|
||||
)
|
||||
|
||||
func TestRequestBaseURLUsesTrustedForwardedValues(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "http://internal:33550/api/client/bootstrap", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "HTTPS, http")
|
||||
req.Header.Set("X-Forwarded-Host", "updates.example.com:8443, internal:33550")
|
||||
|
||||
if got := requestBaseURL(req, "https://fallback.example.com/"); got != "https://updates.example.com:8443" {
|
||||
t.Fatalf("requestBaseURL() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBaseURLRejectsInvalidForwardedValues(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "https://internal:33550/api/client/bootstrap", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "file")
|
||||
req.Header.Set("X-Forwarded-Host", "user@evil.example")
|
||||
if got := requestBaseURL(req, "https://fallback.example.com/"); got != "https://internal:33550" {
|
||||
t.Fatalf("requestBaseURL() = %q", got)
|
||||
}
|
||||
|
||||
req.Host = "bad host"
|
||||
req.Header.Set("X-Forwarded-Host", "updates.example.com:99999")
|
||||
if got := requestBaseURL(req, "https://fallback.example.com/"); got != "https://fallback.example.com" {
|
||||
t.Fatalf("requestBaseURL() with invalid hosts = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSnapshotCacheBoundsHostVariants(t *testing.T) {
|
||||
service := newPublicSnapshotService(time.Hour)
|
||||
for index := 0; index < maxPublicSnapshotEntries+20; index++ {
|
||||
key := fmt.Sprintf("bootstrap|https://host-%d.example.com", index)
|
||||
service.Get(key, func(time.Time) any { return map[string]any{"ok": true} })
|
||||
}
|
||||
if got := len(service.entries); got != maxPublicSnapshotEntries {
|
||||
t.Fatalf("snapshot cache has %d entries, want %d", got, maxPublicSnapshotEntries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSnapshotCacheRemovesExpiredEntries(t *testing.T) {
|
||||
service := newPublicSnapshotService(time.Millisecond)
|
||||
service.Get("expired", func(time.Time) any { return map[string]any{"value": 1} })
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
service.Get("current", func(time.Time) any { return map[string]any{"value": 2} })
|
||||
|
||||
if _, exists := service.entries["expired"]; exists {
|
||||
t.Fatal("expired snapshot was not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAddressTrustsForwardedIPOnlyFromLoopbackProxy(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "http://internal/", nil)
|
||||
req.RemoteAddr = "127.0.0.1:44000"
|
||||
req.Header.Set("X-Real-IP", "203.0.113.8")
|
||||
req.Header.Set("X-Forwarded-For", "198.51.100.2, 203.0.113.9")
|
||||
if got := clientAddress(req); got != "203.0.113.8" {
|
||||
t.Fatalf("clientAddress() = %q", got)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Real-IP", "not-an-ip")
|
||||
if got := clientAddress(req); got != "203.0.113.9" {
|
||||
t.Fatalf("clientAddress() XFF fallback = %q", got)
|
||||
}
|
||||
|
||||
req.RemoteAddr = "192.0.2.25:44000"
|
||||
req.Header.Set("X-Real-IP", "203.0.113.10")
|
||||
if got := clientAddress(req); got != "192.0.2.25" {
|
||||
t.Fatalf("clientAddress() trusted a non-loopback peer: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompatibilityRoutes(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
@@ -54,6 +124,29 @@ func TestCompatibilityRoutes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicAuthBootstrapDoesNotExposeAdministratorCredentials(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/auth/bootstrap", nil)
|
||||
res := httptest.NewRecorder()
|
||||
handler.ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("bootstrap returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, field := range []string{"defaultUsername", "defaultPassword", "isDefaultPassword"} {
|
||||
if _, exists := payload[field]; exists {
|
||||
t.Fatalf("public bootstrap exposed %s: %#v", field, payload)
|
||||
}
|
||||
}
|
||||
if cacheControl := res.Header().Get("Cache-Control"); cacheControl != "no-store" {
|
||||
t.Fatalf("bootstrap cache control = %q, want no-store", cacheControl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientBootstrapSupportsConditionalCaching(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
@@ -246,6 +339,48 @@ func TestClientBootstrapAndEndpointsShape(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReferenceDataSupportsCachingAndBootstrapDiscovery(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/client/reference-data/gpu", nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("reference data returned %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
etag := response.Header().Get("ETag")
|
||||
if etag == "" {
|
||||
t.Fatal("reference data did not include an ETag")
|
||||
}
|
||||
var ranking map[string]any
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &ranking); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ranking["kind"] != "gpu" || ranking["items"] == nil {
|
||||
t.Fatalf("unexpected reference data: %#v", ranking)
|
||||
}
|
||||
|
||||
conditional := httptest.NewRequest(http.MethodGet, "/api/client/reference-data/gpu", nil)
|
||||
conditional.Header.Set("If-None-Match", etag)
|
||||
conditionalResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(conditionalResponse, conditional)
|
||||
if conditionalResponse.Code != http.StatusNotModified {
|
||||
t.Fatalf("conditional reference data returned %d", conditionalResponse.Code)
|
||||
}
|
||||
|
||||
bootstrapRequest := httptest.NewRequest(http.MethodGet, "/api/client/bootstrap", nil)
|
||||
bootstrapResponse := httptest.NewRecorder()
|
||||
handler.ServeHTTP(bootstrapResponse, bootstrapRequest)
|
||||
var bootstrap map[string]any
|
||||
if err := json.Unmarshal(bootstrapResponse.Body.Bytes(), &bootstrap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bootstrap["referenceData"] == nil {
|
||||
t.Fatalf("bootstrap does not advertise reference data: %#v", bootstrap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminDeleteSourcePublishesCompatibilityJSON(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
@@ -653,7 +788,7 @@ func TestAdminReleasePackageUploadRejectsMissingAndOversizedFile(t *testing.T) {
|
||||
{name: "missing", include: false, wantStatus: http.StatusBadRequest, wantCode: "FILE_REQUIRED"},
|
||||
{name: "empty", include: true, data: []byte{}, wantStatus: http.StatusBadRequest, wantCode: "PACKAGE_EMPTY"},
|
||||
{name: "oversized", include: true, data: bytes.Repeat([]byte{'x'}, (1<<20)+1), wantStatus: http.StatusRequestEntityTooLarge, wantCode: "PACKAGE_TOO_LARGE"},
|
||||
{name: "unsafe-name", include: true, data: []byte("package"), wantStatus: http.StatusBadRequest, wantCode: "PACKAGE_UPLOAD_FAILED"},
|
||||
{name: "unsafe-name", include: true, data: []byte("package"), wantStatus: http.StatusBadRequest, wantCode: "PACKAGE_NAME_INVALID"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
@@ -737,8 +872,10 @@ func TestValidateAdminDiskBuildRejectsMissingReferencedAsset(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(`<script type="module" src="/admin/assets/missing.js"></script>`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validateAdminDiskBuild(dir); err == nil || !strings.Contains(err.Error(), "assets/missing.js") {
|
||||
t.Fatalf("validateAdminDiskBuild returned %v, want missing asset error", err)
|
||||
writeAdminBuildMetadata(t, dir, "assets/missing.js")
|
||||
status := adminassets.ValidateDisk(dir, "dev")
|
||||
if status.Ready || !strings.Contains(status.ValidationError, "assets/missing.js") {
|
||||
t.Fatalf("ValidateDisk returned %#v, want missing asset error", status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +890,8 @@ func TestIncompleteAdminDiskBuildDoesNotServeDiskAssets(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(dir, "assets", "stale.js"), []byte(`stale disk asset`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &router{cfg: &config.Config{AdminWebDir: dir}}
|
||||
writeAdminBuildMetadata(t, dir, "assets/missing.js")
|
||||
r := &router{cfg: &config.Config{AdminWebDir: dir, AdminAssetMode: adminassets.ModeDisk}, adminAssets: adminassets.New(adminassets.ModeDisk, dir, "dev")}
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/assets/stale.js", nil)
|
||||
res := httptest.NewRecorder()
|
||||
r.serveAdminAsset(res, req, "assets/stale.js")
|
||||
@@ -768,6 +906,17 @@ func TestIncompleteAdminDiskBuildDoesNotServeDiskAssets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeAdminBuildMetadata(t *testing.T, dir, output string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(dir, "admin-build.json"), []byte(`{"buildId":"dev"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := fmt.Sprintf(`{"src/main.ts":{"file":%q,"isEntry":true}}`, output)
|
||||
if err := os.WriteFile(filepath.Join(dir, "asset-manifest.json"), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func containsAny(value string, needles []string) bool {
|
||||
for _, needle := range needles {
|
||||
if strings.Contains(value, needle) {
|
||||
@@ -913,6 +1062,111 @@ func TestAdminWriteRequiresCSRF(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminPageReadRoutesNeverReturnGatewayOrServerErrors(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
session, _, err := loginForTest(handler)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths := []string{
|
||||
"/api/admin/dashboard/overview?window=24h",
|
||||
"/api/admin/feedbacks?page=1&pageSize=20",
|
||||
"/api/admin/releases",
|
||||
"/api/admin/releases/notices",
|
||||
"/api/admin/legacy/update-info",
|
||||
"/api/admin/legacy/media-types",
|
||||
"/api/admin/sources",
|
||||
"/api/admin/sources/check/status",
|
||||
"/api/admin/endpoints",
|
||||
"/api/admin/database/status",
|
||||
"/api/admin/database/sync/jobs/latest",
|
||||
"/api/admin/system/migration",
|
||||
"/api/admin/system/branding",
|
||||
"/api/admin/system/mail/config",
|
||||
"/api/admin/sync/legacy/preview",
|
||||
"/api/admin/system/health",
|
||||
"/api/admin/system/audit?page=1&pageSize=20",
|
||||
"/api/admin/system/logs?page=1&pageSize=20",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
|
||||
res := httptest.NewRecorder()
|
||||
handler.ServeHTTP(res, req)
|
||||
if res.Code >= http.StatusInternalServerError {
|
||||
t.Fatalf("admin page request returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
if res.Code == http.StatusUnauthorized || res.Code == http.StatusForbidden {
|
||||
t.Fatalf("admin page request returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminDashboardSnapshotAndHealthExposeDiagnostics(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
session, _, err := loginForTest(handler)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
request := func(path string) (*httptest.ResponseRecorder, map[string]any) {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
|
||||
res := httptest.NewRecorder()
|
||||
handler.ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return res, payload
|
||||
}
|
||||
|
||||
first, dashboard := request("/api/admin/dashboard/overview?window=6h")
|
||||
second, _ := request("/api/admin/dashboard/overview?window=6h")
|
||||
for _, key := range []string{"sourceRows", "sourceCheckJobs", "generatedAt", "warnings", "heartbeats", "averageLatency", "clientCalls"} {
|
||||
if _, ok := dashboard[key]; !ok {
|
||||
t.Fatalf("dashboard missing %s: %#v", key, dashboard)
|
||||
}
|
||||
}
|
||||
if first.Header().Get("X-Admin-Cache") != "miss" || second.Header().Get("X-Admin-Cache") != "hit" {
|
||||
t.Fatalf("cache headers first=%q second=%q", first.Header().Get("X-Admin-Cache"), second.Header().Get("X-Admin-Cache"))
|
||||
}
|
||||
_, healthPayload := request("/api/admin/system/health")
|
||||
assets, ok := healthPayload["adminAssets"].(map[string]any)
|
||||
if !ok || assets["manifestStatus"] != "valid" || assets["buildId"] != "dev" {
|
||||
t.Fatalf("admin asset diagnostics missing: %#v", healthPayload["adminAssets"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityMiddlewareRecoversAPIPanicAsJSON(t *testing.T) {
|
||||
handler := withSecurity(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
panic("simulated handler failure")
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/system/health", nil)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(res, req)
|
||||
|
||||
if res.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("status = %d, want %d: %s", res.Code, http.StatusInternalServerError, res.Body.String())
|
||||
}
|
||||
if contentType := res.Header().Get("Content-Type"); !strings.Contains(contentType, "application/json") {
|
||||
t.Fatalf("content type = %q, want JSON", contentType)
|
||||
}
|
||||
if !strings.Contains(res.Body.String(), "INTERNAL_SERVER_ERROR") {
|
||||
t.Fatalf("response does not contain stable error code: %s", res.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func loginForTest(handler http.Handler) (string, string, error) {
|
||||
captchaReq := httptest.NewRequest(http.MethodGet, "/api/admin/auth/captcha", nil)
|
||||
captchaRes := httptest.NewRecorder()
|
||||
@@ -1091,6 +1345,12 @@ func testRouter(t *testing.T) (http.Handler, func()) {
|
||||
if err := os.WriteFile(filepath.Join(adminDist, "assets", "admin.js"), []byte(`console.log("admin")`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(adminDist, "admin-build.json"), []byte(`{"buildId":"dev"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(adminDist, "asset-manifest.json"), []byte(`{"src/main.ts":{"file":"assets/admin.js","css":["assets/admin.css"],"isEntry":true}}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(noticeDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1125,6 +1385,7 @@ func testRouter(t *testing.T) (http.Handler, func()) {
|
||||
UpdateNoticeDir: noticeDir,
|
||||
DownloadsDir: filepath.Join(public, "downloads"),
|
||||
AdminWebDir: adminDist,
|
||||
AdminAssetMode: adminassets.ModeDisk,
|
||||
PortalWebDir: portalDist,
|
||||
SourceCheckSeconds: 3600,
|
||||
ClientSignatureKey: "ymhut-box-feedback-client-v1",
|
||||
|
||||
Reference in New Issue
Block a user