Ctrl K
ComunidadGuías…

backend/server/server_frontend_routes_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package server

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"testing/fstest"

	"github.com/labstack/echo/v5"
)

func newFrontendTestFS() fstest.MapFS {
	return fstest.MapFS{
		"index.html":         &fstest.MapFile{Data: []byte("<!doctype html><html>APP</html>")},
		"assets/app-1234.js": &fstest.MapFile{Data: []byte("console.log('hi')")},
	}
}

func TestRegisterFrontendRoutes_AssetCacheHeaders(t *testing.T) {
	const wantImmutable = "public, max-age=31536000, immutable"
	cases := []struct {
		name             string
		path             string
		wantStatus       int
		wantCacheCtrl    string // "" = asserts no Cache-Control header
		wantBodyContains string
		bodyMustNotBe    string
	}{
		{
			name:          "real asset gets immutable cache",
			path:          "/assets/app-1234.js",
			wantStatus:    http.StatusOK,
			wantCacheCtrl: wantImmutable,
		},
		{
			// 404 must NOT carry the immutable Cache-Control header. In an HA
			// rolling deploy, an older replica can 404 a new hashed asset briefly;
			// browsers/CDNs caching that 404 for a year would wedge users on a
			// broken app long after the deploy completes.
			name:          "missing asset returns 404 with no immutable cache",
			path:          "/assets/this-does-not-exist.js",
			wantStatus:    http.StatusNotFound,
			wantCacheCtrl: "",
			bodyMustNotBe: "<!doctype html",
		},
		{
			// echo v5.4 rejects a path with a dot or empty segment outright
			// (GHSA-3pmx-cf9f-34xr). Deciding the header from the cleaned path
			// instead of the response saw an existing file here and shipped a
			// year-long immutable directive on the handler's 404.
			name:          "escaped dot segment returns 404 with no immutable cache",
			path:          "/assets/x/%2e%2e/app-1234.js",
			wantStatus:    http.StatusNotFound,
			wantCacheCtrl: "",
		},
		{
			name:             "spa route falls back to index.html with no immutable",
			path:             "/projects/foo",
			wantStatus:       http.StatusOK,
			wantCacheCtrl:    "",
			wantBodyContains: "APP",
		},
		{
			name:             "root serves index.html",
			path:             "/",
			wantStatus:       http.StatusOK,
			wantCacheCtrl:    "",
			wantBodyContains: "APP",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			e := echo.New()
			registerFrontendRoutes(e, newFrontendTestFS())

			req := httptest.NewRequest(http.MethodGet, tc.path, nil)
			rec := httptest.NewRecorder()
			e.ServeHTTP(rec, req)

			if rec.Code != tc.wantStatus {
				t.Fatalf("status = %d, want %d; body=%q", rec.Code, tc.wantStatus, rec.Body.String())
			}
			if got := rec.Header().Get(echo.HeaderCacheControl); got != tc.wantCacheCtrl {
				t.Errorf("Cache-Control = %q, want %q", got, tc.wantCacheCtrl)
			}
			body := rec.Body.String()
			if tc.wantBodyContains != "" && !strings.Contains(body, tc.wantBodyContains) {
				t.Errorf("body = %q, want to contain %q", body, tc.wantBodyContains)
			}
			if tc.bodyMustNotBe != "" && strings.Contains(strings.ToLower(body), tc.bodyMustNotBe) {
				t.Errorf("body should not contain %q (HTML5 fallback regression): %q", tc.bodyMustNotBe, body)
			}
		})
	}
}

func TestRegisterFrontendRoutes_AssetHeadRequest(t *testing.T) {
	e := echo.New()
	registerFrontendRoutes(e, newFrontendTestFS())

	req := httptest.NewRequest(http.MethodHead, "/assets/app-1234.js", nil)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("HEAD /assets/app-1234.js status = %d, want 200; body=%q", rec.Code, rec.Body.String())
	}
	if got, want := rec.Header().Get(echo.HeaderCacheControl), "public, max-age=31536000, immutable"; got != want {
		t.Errorf("Cache-Control = %q, want %q", got, want)
	}
}

func TestDefaultAPIRequestSkipper(t *testing.T) {
	cases := map[string]bool{
		// API-ish prefixes that must be skipped (fall through to registered handlers).
		"/api/v1/login":                           true,
		"/v1/projects":                            true,
		"/v1:adminExecute":                        true,
		"/bytebase.v1.AuthService/Login":          true,
		"/bytebase.v1.ProjectService/GetProject":  true,
		"/.well-known/oauth-authorization-server": true,
		"/hook/gitlab":                            true,
		// Paths that must NOT be skipped (they're meant for the static handler).
		"/assets/main-abc.js": false,
		"/projects/foo":       false,
		"/":                   false,
	}
	for reqPath, want := range cases {
		t.Run(reqPath, func(t *testing.T) {
			e := echo.New()
			req := httptest.NewRequest(http.MethodGet, reqPath, nil)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)
			if got := defaultAPIRequestSkipper(c); got != want {
				t.Errorf("defaultAPIRequestSkipper(%q) = %v, want %v", reqPath, got, want)
			}
		})
	}
}

func TestRegisterFrontendRoutes_AssetGzip(t *testing.T) {
	body := strings.Repeat("console.log('hello gzip');\n", 100)
	distFS := fstest.MapFS{
		"index.html":         &fstest.MapFile{Data: []byte("<!doctype html>")},
		"assets/big-1234.js": &fstest.MapFile{Data: []byte(body)},
	}
	e := echo.New()
	registerFrontendRoutes(e, distFS)

	req := httptest.NewRequest(http.MethodGet, "/assets/big-1234.js", nil)
	req.Header.Set(echo.HeaderAcceptEncoding, "gzip")
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%q", rec.Code, rec.Body.String())
	}
	if got := rec.Header().Get(echo.HeaderContentEncoding); got != "gzip" {
		t.Errorf("Content-Encoding = %q, want gzip", got)
	}
}