| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911 |
- //go:build embed
- package web
- import (
- "bytes"
- "context"
- "io/fs"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func init() {
- gin.SetMode(gin.TestMode)
- }
- func TestInjectSiteTitle(t *testing.T) {
- t.Run("replaces_title_with_site_name", func(t *testing.T) {
- html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
- settingsJSON := []byte(`{"site_name":"MyCustomSite"}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Contains(t, string(result), "<title>MyCustomSite - AI API Gateway</title>")
- assert.NotContains(t, string(result), "Sub2API")
- })
- t.Run("returns_unchanged_when_site_name_empty", func(t *testing.T) {
- html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
- settingsJSON := []byte(`{"site_name":""}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Equal(t, string(html), string(result))
- })
- t.Run("returns_unchanged_when_site_name_missing", func(t *testing.T) {
- html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
- settingsJSON := []byte(`{"other_field":"value"}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Equal(t, string(html), string(result))
- })
- t.Run("returns_unchanged_when_invalid_json", func(t *testing.T) {
- html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
- settingsJSON := []byte(`{invalid json}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Equal(t, string(html), string(result))
- })
- t.Run("returns_unchanged_when_no_title_tag", func(t *testing.T) {
- html := []byte(`<html><head></head><body></body></html>`)
- settingsJSON := []byte(`{"site_name":"MyCustomSite"}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Equal(t, string(html), string(result))
- })
- t.Run("returns_unchanged_when_title_has_attributes", func(t *testing.T) {
- // The function looks for "<title>" literally, so attributes are not supported
- // This is acceptable since index.html uses plain <title> without attributes
- html := []byte(`<html><head><title lang="en">Sub2API</title></head><body></body></html>`)
- settingsJSON := []byte(`{"site_name":"NewSite"}`)
- result := injectSiteTitle(html, settingsJSON)
- // Should return unchanged since <title> with attributes is not matched
- assert.Equal(t, string(html), string(result))
- })
- t.Run("escapes_html_in_site_name", func(t *testing.T) {
- html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
- settingsJSON := []byte(`{"site_name":"</title><script>alert(1)</script><title>"}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.NotContains(t, string(result), "<script>")
- assert.Contains(t, string(result), "</title><script>alert(1)</script><title>")
- })
- t.Run("escapes_ampersand_in_site_name", func(t *testing.T) {
- html := []byte(`<html><head><title>Sub2API</title></head><body></body></html>`)
- settingsJSON := []byte(`{"site_name":"A&B"}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Contains(t, string(result), "<title>A&B - AI API Gateway</title>")
- })
- t.Run("preserves_rest_of_html", func(t *testing.T) {
- html := []byte(`<html><head><meta charset="UTF-8"><title>Sub2API</title><script src="app.js"></script></head><body><div id="app"></div></body></html>`)
- settingsJSON := []byte(`{"site_name":"TestSite"}`)
- result := injectSiteTitle(html, settingsJSON)
- assert.Contains(t, string(result), `<meta charset="UTF-8">`)
- assert.Contains(t, string(result), `<script src="app.js"></script>`)
- assert.Contains(t, string(result), `<div id="app"></div>`)
- assert.Contains(t, string(result), "<title>TestSite - AI API Gateway</title>")
- })
- }
- func TestInjectSiteFavicon(t *testing.T) {
- t.Run("replaces_favicon_with_site_logo", func(t *testing.T) {
- html := []byte(`<html><head><link rel="icon" type="image/png" href="/logo.png" /></head></html>`)
- settingsJSON := []byte(`{"site_logo":"https://example.com/custom-logo.png"}`)
- result := injectSiteFavicon(html, settingsJSON)
- assert.Contains(t, string(result), `<link rel="icon" href="https://example.com/custom-logo.png" />`)
- assert.NotContains(t, string(result), `/logo.png`)
- })
- t.Run("supports_relative_and_data_image_urls", func(t *testing.T) {
- html := []byte(`<link rel="icon" href="/logo.png" />`)
- assert.Contains(t, string(injectSiteFavicon(html, []byte(`{"site_logo":"/uploads/logo.svg"}`))), `/uploads/logo.svg`)
- assert.Contains(t, string(injectSiteFavicon(html, []byte(`{"site_logo":"data:image/png;base64,abc"}`))), `data:image/png;base64,abc`)
- })
- t.Run("rejects_unsafe_logo_urls", func(t *testing.T) {
- html := []byte(`<link rel="icon" href="/logo.png" />`)
- result := injectSiteFavicon(html, []byte(`{"site_logo":"javascript:alert(1)"}`))
- assert.Equal(t, string(html), string(result))
- })
- t.Run("escapes_logo_url_for_html", func(t *testing.T) {
- html := []byte(`<link rel="icon" href="/logo.png" />`)
- result := injectSiteFavicon(html, []byte(`{"site_logo":"https://example.com/logo.png?a=1&b=2"}`))
- assert.Contains(t, string(result), `a=1&b=2`)
- })
- }
- func TestReplaceNoncePlaceholder(t *testing.T) {
- t.Run("replaces_single_placeholder", func(t *testing.T) {
- html := []byte(`<script nonce="__CSP_NONCE_VALUE__">console.log('test');</script>`)
- nonce := "abc123xyz"
- result := replaceNoncePlaceholder(html, nonce)
- expected := `<script nonce="abc123xyz">console.log('test');</script>`
- assert.Equal(t, expected, string(result))
- })
- t.Run("replaces_multiple_placeholders", func(t *testing.T) {
- html := []byte(`<script nonce="__CSP_NONCE_VALUE__">a</script><script nonce="__CSP_NONCE_VALUE__">b</script>`)
- nonce := "nonce123"
- result := replaceNoncePlaceholder(html, nonce)
- assert.Equal(t, 2, strings.Count(string(result), `nonce="nonce123"`))
- assert.NotContains(t, string(result), NonceHTMLPlaceholder)
- })
- t.Run("handles_empty_nonce", func(t *testing.T) {
- html := []byte(`<script nonce="__CSP_NONCE_VALUE__">test</script>`)
- nonce := ""
- result := replaceNoncePlaceholder(html, nonce)
- assert.Equal(t, `<script nonce="">test</script>`, string(result))
- })
- t.Run("no_placeholder_returns_unchanged", func(t *testing.T) {
- html := []byte(`<script>console.log('test');</script>`)
- nonce := "abc123"
- result := replaceNoncePlaceholder(html, nonce)
- assert.Equal(t, string(html), string(result))
- })
- t.Run("handles_empty_html", func(t *testing.T) {
- html := []byte(``)
- nonce := "abc123"
- result := replaceNoncePlaceholder(html, nonce)
- assert.Empty(t, result)
- })
- }
- func TestNonceHTMLPlaceholder(t *testing.T) {
- t.Run("constant_value", func(t *testing.T) {
- assert.Equal(t, "__CSP_NONCE_VALUE__", NonceHTMLPlaceholder)
- })
- }
- // mockSettingsProvider implements PublicSettingsProvider for testing
- type mockSettingsProvider struct {
- settings any
- err error
- called int
- }
- func (m *mockSettingsProvider) GetPublicSettingsForInjection(ctx context.Context) (any, error) {
- m.called++
- return m.settings, m.err
- }
- func TestFrontendServer_InjectSettings(t *testing.T) {
- t.Run("injects_settings_with_nonce_placeholder", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"key": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- settingsJSON := []byte(`{"test":"data"}`)
- result := server.injectSettings(settingsJSON)
- // Should contain the script with nonce placeholder
- assert.Contains(t, string(result), `<script nonce="__CSP_NONCE_VALUE__">`)
- assert.Contains(t, string(result), `window.__APP_CONFIG__={"test":"data"};`)
- assert.Contains(t, string(result), `</script></head>`)
- })
- t.Run("injects_before_head_close", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"key": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- settingsJSON := []byte(`{}`)
- result := server.injectSettings(settingsJSON)
- // Script should be injected before </head>
- headCloseIndex := bytes.Index(result, []byte("</head>"))
- scriptIndex := bytes.Index(result, []byte(`<script nonce="`))
- assert.True(t, scriptIndex < headCloseIndex, "script should be before </head>")
- })
- t.Run("handles_complex_settings", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]any{
- "nested": map[string]any{
- "array": []int{1, 2, 3},
- },
- },
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- settingsJSON := []byte(`{"nested":{"array":[1,2,3]},"special":"<>&"}`)
- result := server.injectSettings(settingsJSON)
- assert.Contains(t, string(result), `window.__APP_CONFIG__={"nested":{"array":[1,2,3]},"special":"<>&"};`)
- })
- }
- func TestFrontendServer_ServeIndexHTML(t *testing.T) {
- t.Run("serves_html_with_nonce", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- // Create a gin context with nonce
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- // Set nonce in context (simulating SecurityHeaders middleware)
- testNonce := "test-nonce-12345"
- c.Set(middleware.CSPNonceKey, testNonce)
- server.serveIndexHTML(c)
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "text/html")
- body := w.Body.String()
- // Nonce placeholder should be replaced
- assert.NotContains(t, body, NonceHTMLPlaceholder)
- assert.Contains(t, body, `nonce="`+testNonce+`"`)
- })
- t.Run("caches_html_content", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- // First request
- w1 := httptest.NewRecorder()
- c1, _ := gin.CreateTestContext(w1)
- c1.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c1.Set(middleware.CSPNonceKey, "nonce1")
- server.serveIndexHTML(c1)
- assert.Equal(t, 1, provider.called)
- // Second request - should use cache
- w2 := httptest.NewRecorder()
- c2, _ := gin.CreateTestContext(w2)
- c2.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c2.Set(middleware.CSPNonceKey, "nonce2")
- server.serveIndexHTML(c2)
- // Settings provider should not be called again
- assert.Equal(t, 1, provider.called)
- // But nonce should be different
- assert.Contains(t, w2.Body.String(), `nonce="nonce2"`)
- })
- t.Run("sets_etag_header", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c.Set(middleware.CSPNonceKey, "nonce123")
- server.serveIndexHTML(c)
- etag := w.Header().Get("ETag")
- assert.NotEmpty(t, etag)
- assert.True(t, strings.HasPrefix(etag, `"`))
- assert.True(t, strings.HasSuffix(etag, `"`))
- })
- t.Run("returns_304_for_matching_etag", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- // Use a real router for proper 304 handling
- router := gin.New()
- router.Use(func(c *gin.Context) {
- c.Set(middleware.CSPNonceKey, "test-nonce")
- c.Next()
- })
- router.Use(server.Middleware())
- // First request to populate cache and get ETag
- w1 := httptest.NewRecorder()
- req1 := httptest.NewRequest(http.MethodGet, "/", nil)
- router.ServeHTTP(w1, req1)
- etag := w1.Header().Get("ETag")
- require.NotEmpty(t, etag)
- // Second request with If-None-Match
- w2 := httptest.NewRecorder()
- req2 := httptest.NewRequest(http.MethodGet, "/", nil)
- req2.Header.Set("If-None-Match", etag)
- router.ServeHTTP(w2, req2)
- assert.Equal(t, http.StatusNotModified, w2.Code)
- assert.Empty(t, w2.Body.String())
- })
- t.Run("sets_cache_control_header", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c.Set(middleware.CSPNonceKey, "nonce123")
- server.serveIndexHTML(c)
- assert.Equal(t, "no-cache", w.Header().Get("Cache-Control"))
- })
- t.Run("fallback_on_settings_error", func(t *testing.T) {
- provider := &mockSettingsProvider{
- err: context.DeadlineExceeded,
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- // Invalidate cache to force settings fetch
- server.InvalidateCache()
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c.Set(middleware.CSPNonceKey, "nonce123")
- server.serveIndexHTML(c)
- // Should still return 200 with base HTML
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "text/html")
- })
- }
- func TestFrontendServer_InvalidateCache(t *testing.T) {
- t.Run("invalidates_cache", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- // First request to populate cache
- w1 := httptest.NewRecorder()
- c1, _ := gin.CreateTestContext(w1)
- c1.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c1.Set(middleware.CSPNonceKey, "nonce1")
- server.serveIndexHTML(c1)
- assert.Equal(t, 1, provider.called)
- // Invalidate cache
- server.InvalidateCache()
- // Update settings
- provider.settings = map[string]string{"test": "new_value"}
- // Second request should fetch new settings
- w2 := httptest.NewRecorder()
- c2, _ := gin.CreateTestContext(w2)
- c2.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c2.Set(middleware.CSPNonceKey, "nonce2")
- server.serveIndexHTML(c2)
- assert.Equal(t, 2, provider.called)
- })
- t.Run("handles_nil_server", func(t *testing.T) {
- var server *FrontendServer
- // Should not panic
- assert.NotPanics(t, func() {
- server.InvalidateCache()
- })
- })
- t.Run("handles_nil_cache", func(t *testing.T) {
- server := &FrontendServer{}
- // Should not panic
- assert.NotPanics(t, func() {
- server.InvalidateCache()
- })
- })
- }
- func TestOverrideFilesNeverReceiveImmutableCacheHeaders(t *testing.T) {
- t.Parallel()
- overrideDir := t.TempDir()
- cleanPath := "assets/index-AbCd1234.js"
- filePath := filepath.Join(overrideDir, cleanPath)
- require.NoError(t, os.MkdirAll(filepath.Dir(filePath), 0o755))
- require.NoError(t, os.WriteFile(filePath, []byte("override"), 0o644))
- t.Run("frontend_server_override", func(t *testing.T) {
- t.Parallel()
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/"+cleanPath, nil)
- server := &FrontendServer{overrideDir: overrideDir}
- assert.True(t, server.tryServeOverride(c, cleanPath))
- assert.Empty(t, w.Header().Get("Cache-Control"))
- })
- t.Run("legacy_override", func(t *testing.T) {
- t.Parallel()
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/"+cleanPath, nil)
- assert.True(t, tryServeOverrideFile(c, overrideDir, cleanPath))
- assert.Empty(t, w.Header().Get("Cache-Control"))
- })
- }
- func TestFrontendServer_Middleware(t *testing.T) {
- t.Run("skips_api_routes", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- apiPaths := []string{
- "/api/v1/users",
- "/models",
- "/v1/models",
- "/v1beta/chat",
- "/backend-api/codex/responses",
- "/backend-api/codex/responses/compact",
- "/antigravity/test",
- "/setup/init",
- "/health",
- "/responses",
- "/responses/compact",
- }
- for _, path := range apiPaths {
- t.Run(path, func(t *testing.T) {
- router := gin.New()
- router.Use(server.Middleware())
- nextCalled := false
- router.GET(path, func(c *gin.Context) {
- nextCalled = true
- c.String(http.StatusOK, "ok")
- })
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, path, nil)
- router.ServeHTTP(w, req)
- assert.True(t, nextCalled, "next handler should be called for API route")
- })
- }
- })
- t.Run("skips_responses_compact_post_routes", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- router := gin.New()
- router.Use(server.Middleware())
- nextCalled := false
- router.POST("/responses/compact", func(c *gin.Context) {
- nextCalled = true
- c.String(http.StatusOK, `{"ok":true}`)
- })
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/responses/compact", strings.NewReader(`{"model":"gpt-5"}`))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(w, req)
- assert.True(t, nextCalled, "next handler should be called for compact API route")
- assert.Equal(t, http.StatusOK, w.Code)
- assert.JSONEq(t, `{"ok":true}`, w.Body.String())
- })
- t.Run("skips_alpha_search_post_route", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- router := gin.New()
- router.Use(server.Middleware())
- nextCalled := false
- router.POST("/alpha/search", func(c *gin.Context) {
- nextCalled = true
- c.JSON(http.StatusOK, gin.H{"ok": true})
- })
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/alpha/search", strings.NewReader(`{"model":"gpt-5.6-sol"}`))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(w, req)
- assert.True(t, nextCalled, "next handler should be called for alpha search API route")
- assert.Equal(t, http.StatusOK, w.Code)
- assert.JSONEq(t, `{"ok":true}`, w.Body.String())
- })
- t.Run("serves_index_for_spa_routes", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- router := gin.New()
- router.Use(func(c *gin.Context) {
- c.Set(middleware.CSPNonceKey, "test-nonce")
- c.Next()
- })
- router.Use(server.Middleware())
- spaPaths := []string{
- "/",
- "/dashboard",
- "/users/123",
- "/settings/profile",
- }
- for _, path := range spaPaths {
- t.Run(path, func(t *testing.T) {
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, path, nil)
- router.ServeHTTP(w, req)
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "text/html")
- })
- }
- })
- t.Run("serves_static_files", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- router := gin.New()
- router.Use(server.Middleware())
- // Request for existing static file
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/logo.png", nil)
- router.ServeHTTP(w, req)
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "image/png")
- assert.Empty(t, w.Header().Get("Cache-Control"))
- entries, err := fs.ReadDir(server.distFS, "assets")
- require.NoError(t, err)
- fingerprintedPath := ""
- for _, entry := range entries {
- candidate := "assets/" + entry.Name()
- if !entry.IsDir() && isFingerprintedEmbeddedAssetPath(candidate) {
- fingerprintedPath = candidate
- break
- }
- }
- require.NotEmpty(t, fingerprintedPath)
- assetWriter := httptest.NewRecorder()
- assetRequest := httptest.NewRequest(http.MethodGet, "/"+fingerprintedPath, nil)
- router.ServeHTTP(assetWriter, assetRequest)
- assert.Equal(t, http.StatusOK, assetWriter.Code)
- assert.Equal(t, staticAssetsCacheControl, assetWriter.Header().Get("Cache-Control"))
- })
- }
- func TestEmbeddedFrontendBypassesBareVideoAPIRoutes(t *testing.T) {
- for _, path := range []string{
- "/videos/generations",
- "/videos/edits",
- "/videos/extensions",
- "/videos/request-123",
- } {
- require.True(t, shouldBypassEmbeddedFrontend(path), "path=%s", path)
- }
- }
- func TestNewFrontendServer(t *testing.T) {
- t.Run("creates_server_successfully", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- assert.NotNil(t, server)
- assert.NotNil(t, server.distFS)
- assert.NotNil(t, server.fileServer)
- assert.NotNil(t, server.baseHTML)
- assert.NotNil(t, server.cache)
- assert.Equal(t, provider, server.settings)
- })
- t.Run("reads_base_html", func(t *testing.T) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, err := NewFrontendServer(provider)
- require.NoError(t, err)
- assert.NotEmpty(t, server.baseHTML)
- assert.Contains(t, string(server.baseHTML), "<!doctype html>")
- })
- }
- func TestHasEmbeddedFrontend(t *testing.T) {
- t.Run("returns_true_when_frontend_embedded", func(t *testing.T) {
- result := HasEmbeddedFrontend()
- assert.True(t, result)
- })
- }
- // Tests for legacy ServeEmbeddedFrontend function
- func TestServeEmbeddedFrontend(t *testing.T) {
- t.Run("serves_static_files", func(t *testing.T) {
- middleware := ServeEmbeddedFrontend()
- router := gin.New()
- router.Use(middleware)
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/logo.png", nil)
- router.ServeHTTP(w, req)
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "image/png")
- })
- t.Run("serves_index_html_for_root", func(t *testing.T) {
- middleware := ServeEmbeddedFrontend()
- router := gin.New()
- router.Use(middleware)
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/", nil)
- router.ServeHTTP(w, req)
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "text/html")
- assert.Contains(t, w.Body.String(), "<!doctype html>")
- })
- t.Run("serves_index_html_for_spa_routes", func(t *testing.T) {
- middleware := ServeEmbeddedFrontend()
- router := gin.New()
- router.Use(middleware)
- spaPaths := []string{"/dashboard", "/users/123", "/settings"}
- for _, path := range spaPaths {
- t.Run(path, func(t *testing.T) {
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, path, nil)
- router.ServeHTTP(w, req)
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Contains(t, w.Header().Get("Content-Type"), "text/html")
- })
- }
- })
- t.Run("skips_api_routes", func(t *testing.T) {
- middleware := ServeEmbeddedFrontend()
- apiPaths := []string{
- "/api/users",
- "/models",
- "/v1/models",
- "/v1beta/chat",
- "/backend-api/codex/responses",
- "/backend-api/codex/responses/compact",
- "/antigravity/test",
- "/setup/init",
- "/health",
- "/responses",
- "/responses/compact",
- }
- for _, path := range apiPaths {
- t.Run(path, func(t *testing.T) {
- nextCalled := false
- router := gin.New()
- router.Use(middleware)
- router.GET(path, func(c *gin.Context) {
- nextCalled = true
- c.String(http.StatusOK, "ok")
- })
- w := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, path, nil)
- router.ServeHTTP(w, req)
- assert.True(t, nextCalled, "next handler should be called for API route")
- })
- }
- })
- }
- // Tests for HTMLCache
- func TestHTMLCache(t *testing.T) {
- t.Run("new_cache_returns_nil", func(t *testing.T) {
- cache := NewHTMLCache()
- assert.Nil(t, cache.Get())
- })
- t.Run("set_and_get", func(t *testing.T) {
- cache := NewHTMLCache()
- cache.SetBaseHTML([]byte("<html></html>"))
- html := []byte("<html><body>test</body></html>")
- settings := []byte(`{"key":"value"}`)
- cache.Set(html, settings)
- result := cache.Get()
- require.NotNil(t, result)
- assert.Equal(t, html, result.Content)
- assert.NotEmpty(t, result.ETag)
- })
- t.Run("invalidate_clears_cache", func(t *testing.T) {
- cache := NewHTMLCache()
- cache.SetBaseHTML([]byte("<html></html>"))
- html := []byte("<html><body>test</body></html>")
- settings := []byte(`{"key":"value"}`)
- cache.Set(html, settings)
- require.NotNil(t, cache.Get())
- cache.Invalidate()
- assert.Nil(t, cache.Get())
- })
- t.Run("etag_changes_with_settings", func(t *testing.T) {
- cache := NewHTMLCache()
- cache.SetBaseHTML([]byte("<html></html>"))
- html := []byte("<html><body>test</body></html>")
- cache.Set(html, []byte(`{"v":1}`))
- etag1 := cache.Get().ETag
- cache.Invalidate()
- cache.Set(html, []byte(`{"v":2}`))
- etag2 := cache.Get().ETag
- assert.NotEqual(t, etag1, etag2)
- })
- t.Run("etag_format", func(t *testing.T) {
- cache := NewHTMLCache()
- cache.SetBaseHTML([]byte("<html></html>"))
- cache.Set([]byte("<html></html>"), []byte(`{}`))
- result := cache.Get()
- // ETag should be quoted
- assert.True(t, strings.HasPrefix(result.ETag, `"`))
- assert.True(t, strings.HasSuffix(result.ETag, `"`))
- // Should contain dash separator
- assert.Contains(t, result.ETag[1:len(result.ETag)-1], "-")
- })
- }
- // Benchmark tests
- func BenchmarkReplaceNoncePlaceholder(b *testing.B) {
- html := []byte(`<!DOCTYPE html><html><head><script nonce="__CSP_NONCE_VALUE__">window.__APP_CONFIG__={"test":"data"};</script></head><body></body></html>`)
- nonce := "abcdefghijklmnop123456=="
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- replaceNoncePlaceholder(html, nonce)
- }
- }
- func BenchmarkFrontendServerServeIndexHTML(b *testing.B) {
- provider := &mockSettingsProvider{
- settings: map[string]string{"test": "value"},
- }
- server, _ := NewFrontendServer(provider)
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- c.Set(middleware.CSPNonceKey, "test-nonce")
- server.serveIndexHTML(c)
- }
- }
|