@@ -4,15 +4,21 @@ import (
44 "net/http"
55 "net/http/httptest"
66 "testing"
7+ "testing/fstest"
78
89 "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
911)
1012
11- // These tests handle two valid states:
12- // - Clean checkout / CI / make verify: dist has only .gitkeep → Available() = false
13- // - After make frontend-build: dist has built assets → Available() = true
14- //
15- // make verify runs embed-clean first, so the CI path is always exercised.
13+ // testFS builds a synthetic in-memory filesystem for testing the SPA handler
14+ // independent of whether the real frontend was built.
15+ func testFS () fstest.MapFS {
16+ return fstest.MapFS {
17+ "index.html" : {Data : []byte ("<html>SPA</html>" )},
18+ "assets/app.js" : {Data : []byte ("console.log('app')" )},
19+ "assets/style.css" : {Data : []byte ("body{}" )},
20+ }
21+ }
1622
1723func TestAvailable (t * testing.T ) {
1824 // Available() returns whether built frontend assets are embedded.
@@ -29,33 +35,82 @@ func TestHandler_ReturnsHandler(t *testing.T) {
2935 assert .NotNil (t , h )
3036}
3137
32- func TestHandler_Root (t * testing.T ) {
33- h := Handler ( )
38+ func TestSPAHandler_Root_ServesIndexHTML (t * testing.T ) {
39+ h := newSPAHandler ( testFS () )
3440
3541 req := httptest .NewRequest (http .MethodGet , "/" , http .NoBody )
3642 rec := httptest .NewRecorder ()
3743 h .ServeHTTP (rec , req )
3844
39- if Available () {
40- // SPA fallback rewrites to /index.html; http.FileServer redirects
41- // /index.html back to ./ (Go hides the default index file).
42- assert .Equal (t , http .StatusMovedPermanently , rec .Code )
43- } else {
44- // No index.html in dist — SPA fallback returns 404.
45- assert .Equal (t , http .StatusNotFound , rec .Code )
46- }
45+ assert .Equal (t , http .StatusOK , rec .Code )
46+ assert .Contains (t , rec .Header ().Get ("Content-Type" ), "text/html" )
47+ assert .Contains (t , rec .Body .String (), "<html>SPA</html>" )
4748}
4849
49- func TestHandler_SPAFallback (t * testing.T ) {
50- h := Handler ( )
50+ func TestSPAHandler_SPAFallback_ServesIndexHTML (t * testing.T ) {
51+ h := newSPAHandler ( testFS () )
5152
5253 req := httptest .NewRequest (http .MethodGet , "/dashboard" , http .NoBody )
5354 rec := httptest .NewRecorder ()
5455 h .ServeHTTP (rec , req )
5556
56- if Available () {
57- assert .Equal (t , http .StatusMovedPermanently , rec .Code )
58- } else {
59- assert .Equal (t , http .StatusNotFound , rec .Code )
60- }
57+ assert .Equal (t , http .StatusOK , rec .Code )
58+ assert .Contains (t , rec .Header ().Get ("Content-Type" ), "text/html" )
59+ assert .Contains (t , rec .Body .String (), "<html>SPA</html>" )
60+ }
61+
62+ func TestSPAHandler_StaticAsset_ServedByFileServer (t * testing.T ) {
63+ h := newSPAHandler (testFS ())
64+
65+ req := httptest .NewRequest (http .MethodGet , "/assets/app.js" , http .NoBody )
66+ rec := httptest .NewRecorder ()
67+ h .ServeHTTP (rec , req )
68+
69+ assert .Equal (t , http .StatusOK , rec .Code )
70+ assert .Contains (t , rec .Body .String (), "console.log" )
71+ }
72+
73+ func TestSPAHandler_CSSAsset (t * testing.T ) {
74+ h := newSPAHandler (testFS ())
75+
76+ req := httptest .NewRequest (http .MethodGet , "/assets/style.css" , http .NoBody )
77+ rec := httptest .NewRecorder ()
78+ h .ServeHTTP (rec , req )
79+
80+ assert .Equal (t , http .StatusOK , rec .Code )
81+ assert .Contains (t , rec .Body .String (), "body{}" )
82+ }
83+
84+ func TestSPAHandler_NoIndexHTML_Returns404 (t * testing.T ) {
85+ emptyFS := fstest.MapFS {}
86+ h := newSPAHandler (emptyFS )
87+
88+ req := httptest .NewRequest (http .MethodGet , "/" , http .NoBody )
89+ rec := httptest .NewRecorder ()
90+ h .ServeHTTP (rec , req )
91+
92+ assert .Equal (t , http .StatusNotFound , rec .Code )
93+ }
94+
95+ func TestSPAHandler_NoRedirectLoop (t * testing.T ) {
96+ h := newSPAHandler (testFS ())
97+
98+ // The exact bug: /index.html must NOT produce a 301
99+ req := httptest .NewRequest (http .MethodGet , "/index.html" , http .NoBody )
100+ rec := httptest .NewRecorder ()
101+ h .ServeHTTP (rec , req )
102+
103+ // /index.html is a real file, so FileServer serves it (200), not redirect
104+ require .Equal (t , http .StatusOK , rec .Code , "index.html must not redirect" )
105+ }
106+
107+ func TestSPAHandler_NestedUnknownRoute_Fallback (t * testing.T ) {
108+ h := newSPAHandler (testFS ())
109+
110+ req := httptest .NewRequest (http .MethodGet , "/settings/profile" , http .NoBody )
111+ rec := httptest .NewRecorder ()
112+ h .ServeHTTP (rec , req )
113+
114+ assert .Equal (t , http .StatusOK , rec .Code )
115+ assert .Contains (t , rec .Body .String (), "<html>SPA</html>" )
61116}
0 commit comments