//go:build embed package web import ( "bytes" "context" "net/http" "net/http/httptest" "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(`Sub2API - AI API Gateway`) settingsJSON := []byte(`{"site_name":"MyCustomSite"}`) result := injectSiteTitle(html, settingsJSON) assert.Contains(t, string(result), "MyCustomSite - AI API Gateway") assert.NotContains(t, string(result), "Sub2API") }) t.Run("returns_unchanged_when_site_name_empty", func(t *testing.T) { html := []byte(`Sub2API - AI API Gateway`) 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(`Sub2API - AI API Gateway`) 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(`Sub2API - AI API Gateway`) 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(``) 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 "" 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`) settingsJSON := []byte(`{"site_name":"NewSite"}`) result := injectSiteTitle(html, settingsJSON) // Should return unchanged since with attributes is not matched assert.Equal(t, string(html), string(result)) }) t.Run("preserves_rest_of_html", func(t *testing.T) { html := []byte(`<html><head><meta charset="UTF-8"><title>Sub2API
`) settingsJSON := []byte(`{"site_name":"TestSite"}`) result := injectSiteTitle(html, settingsJSON) assert.Contains(t, string(result), ``) assert.Contains(t, string(result), ``) assert.Contains(t, string(result), `
`) assert.Contains(t, string(result), "TestSite - AI API Gateway") }) } func TestReplaceNoncePlaceholder(t *testing.T) { t.Run("replaces_single_placeholder", func(t *testing.T) { html := []byte(``) nonce := "abc123xyz" result := replaceNoncePlaceholder(html, nonce) expected := `` assert.Equal(t, expected, string(result)) }) t.Run("replaces_multiple_placeholders", func(t *testing.T) { html := []byte(``) 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(``) nonce := "" result := replaceNoncePlaceholder(html, nonce) assert.Equal(t, ``, string(result)) }) t.Run("no_placeholder_returns_unchanged", func(t *testing.T) { html := []byte(``) 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), ``) }) 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 headCloseIndex := bytes.Index(result, []byte("")) scriptIndex := bytes.Index(result, []byte(``) 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) } }