Skip to content

Commit 427672b

Browse files
committed
Additional test cases
1 parent 8bd5804 commit 427672b

File tree

2 files changed

+135
-29
lines changed

2 files changed

+135
-29
lines changed

site/embed.go

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"embed"
1010
"fmt"
11+
"html/template"
1112
"io/fs"
1213
"net/http"
1314
"path/filepath"
@@ -69,10 +70,18 @@ func Handler(filesystem fs.FS, logger slog.Logger, templateFunc HTMLTemplateHand
6970
}
7071

7172
func serveFiles(fileSystem fs.FS, logger slog.Logger) (http.HandlerFunc, error) {
73+
// htmlFileToTemplate is a map of html files -> template
74+
// We need to use templates in order to inject parameters from `HtmlState`
75+
// (like CSRF token and CSP nonce)
76+
htmlFileToTemplate := map[string]*template.Template{}
7277

73-
fileNameToBytes := map[string][]byte{}
74-
var indexBytes []byte
75-
indexBytes = nil
78+
// nonHtmlFileToTemplate is a map of files -> byte contents
79+
// This is used for any non-HTML file
80+
nonHtmlFileToTemplate := map[string][]byte{}
81+
82+
// fallbackHtmlTemplate is used as the 'default' template if
83+
// the path requested doesn't match anything on the file systme.
84+
var fallbackHtmlTemplate *template.Template
7685

7786
files, err := fs.ReadDir(fileSystem, ".")
7887
if err != nil {
@@ -93,19 +102,37 @@ func serveFiles(fileSystem fs.FS, logger slog.Logger) (http.HandlerFunc, error)
93102
continue
94103
}
95104

96-
fileNameToBytes[normalizedName] = fileBytes
97-
if normalizedName == "index.html" {
98-
indexBytes = fileBytes
105+
isHtml := isHtmlFile(normalizedName)
106+
if isHtml {
107+
// For HTML files, we need to parse and store the template.
108+
// If its index.html, we need to keep a reference to it as well.
109+
template, err := template.New("").Parse(string(fileBytes))
110+
if err != nil {
111+
logger.Warn(context.Background(), "Unable to parse html template", slog.F("fileName", normalizedName))
112+
continue
113+
}
114+
115+
htmlFileToTemplate[normalizedName] = template
116+
// If this is the index page, use it as the fallback template
117+
if strings.HasPrefix(normalizedName, "index.") {
118+
fallbackHtmlTemplate = template
119+
}
120+
} else {
121+
// Non HTML files are easy - just cache the bytes
122+
nonHtmlFileToTemplate[normalizedName] = fileBytes
99123
}
100124

101125
continue
102126
}
103127

128+
// If we reached here, there was something on the file system (most likely a directory)
129+
// that we were unable to handle in the current code - so log a warning.
104130
logger.Warn(context.Background(), "Serving from nested directories is not implemented", slog.F("name", name))
105131
}
106132

107-
if indexBytes == nil {
108-
return nil, xerrors.Errorf("No index.html available")
133+
// If we don't have a default template, then there's not much to do!
134+
if fallbackHtmlTemplate == nil {
135+
return nil, xerrors.Errorf("No index.html found")
109136
}
110137

111138
serveFunc := func(writer http.ResponseWriter, request *http.Request) {
@@ -116,29 +143,63 @@ func serveFiles(fileSystem fs.FS, logger slog.Logger) (http.HandlerFunc, error)
116143
normalizedFileName = "index.html"
117144
}
118145

119-
isCacheable := !strings.HasSuffix(normalizedFileName, ".html") && !strings.HasSuffix(normalizedFileName, ".htm")
120-
121-
fileBytes, ok := fileNameToBytes[normalizedFileName]
122-
if !ok {
123-
logger.Warn(request.Context(), "Unable to find request file", slog.F("fileName", normalizedFileName))
124-
fileBytes = indexBytes
125-
isCacheable = false
126-
normalizedFileName = "index.html"
127-
}
128-
129-
if isCacheable {
146+
// First, let's look at our non-HTML files to see if this matches
147+
fileBytes, ok := nonHtmlFileToTemplate[normalizedFileName]
148+
if ok {
130149
// All our assets - JavaScript, CSS, images - should be cached.
131150
// For cases like JavaScript, we rely on a cache-busting strategy whenever
132151
// there is a new version (this is handled in our webpack config).
133152
writer.Header().Add("Cache-Control", "public, max-age=31536000, immutable")
153+
http.ServeContent(writer, request, normalizedFileName, time.Time{}, bytes.NewReader(fileBytes))
154+
return
134155
}
135156

136-
http.ServeContent(writer, request, normalizedFileName, time.Time{}, bytes.NewReader(fileBytes))
157+
var buf bytes.Buffer
158+
// TODO: Fix this
159+
templateData := HtmlState{
160+
CSRFToken: "TODO",
161+
CSPNonce: "TODO",
162+
}
163+
164+
// Next, lets try and load from our HTML templates
165+
template, ok := htmlFileToTemplate[normalizedFileName]
166+
if ok {
167+
logger.Debug(context.Background(), "Applying template parameters", slog.F("fileName", normalizedFileName), slog.F("templateData", templateData))
168+
err := template.ExecuteTemplate(&buf, "", templateData)
169+
170+
if err != nil {
171+
logger.Error(request.Context(), "Error executing template", slog.F("templateData", templateData))
172+
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
173+
return
174+
}
175+
176+
http.ServeContent(writer, request, normalizedFileName, time.Time{}, bytes.NewReader(buf.Bytes()))
177+
return
178+
}
179+
180+
// Finally... the path didn't match any file that we had cached.
181+
// This is expected, because any nested path is going to hit this case.
182+
// For that, we'll serve the fallback
183+
logger.Debug(context.Background(), "Applying template parameters", slog.F("fileName", normalizedFileName), slog.F("templateData", templateData))
184+
err := fallbackHtmlTemplate.ExecuteTemplate(&buf, "", templateData)
185+
186+
if err != nil {
187+
logger.Error(request.Context(), "Error executing template", slog.F("templateData", templateData))
188+
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
189+
return
190+
}
191+
192+
http.ServeContent(writer, request, normalizedFileName, time.Time{}, bytes.NewReader(buf.Bytes()))
193+
137194
}
138195

139196
return serveFunc, nil
140197
}
141198

199+
func isHtmlFile(fileName string) bool {
200+
return strings.HasSuffix(fileName, ".html") || strings.HasSuffix(fileName, ".htm")
201+
}
202+
142203
type HtmlState struct {
143204
CSPNonce string
144205
CSRFToken string

site/embed_test.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,48 @@ func TestNestedPathsRenderIndex(t *testing.T) {
4646
"index.html": &fstest.MapFile{
4747
Data: []byte("index-test-file"),
4848
},
49+
"favicon.ico": &fstest.MapFile{
50+
Data: []byte("favicon-bytes"),
51+
},
52+
}
53+
54+
var nestedPathTests = []struct {
55+
path string
56+
expected string
57+
}{
58+
// HTML cases
59+
{"/index.html", "index-test-file"},
60+
{"/", "index-test-file"},
61+
{"/nested/index.html", "index-test-file"},
62+
{"/nested", "index-test-file"},
63+
{"/nested/", "index-test-file"},
64+
{"/double/nested/index.html", "index-test-file"},
65+
{"/double/nested", "index-test-file"},
66+
{"/double/nested/", "index-test-file"},
67+
68+
// Other file cases
69+
{"/favicon.ico", "favicon-bytes"},
70+
// Ensure that nested still picks up the 'top-level' file
71+
{"/nested/favicon.ico", "favicon-bytes"},
72+
{"/double/nested/favicon.ico", "favicon-bytes"},
4973
}
5074

5175
srv := httptest.NewServer(site.Handler(rootFS, slog.Logger{}, defaultTemplateFunc))
5276

53-
path := srv.URL + "/some/nested/path"
77+
for _, testCase := range nestedPathTests {
78+
path := srv.URL + testCase.path
5479

55-
req, err := http.NewRequestWithContext(context.Background(), "GET", path, nil)
56-
require.NoError(t, err)
57-
resp, err := http.DefaultClient.Do(req)
58-
require.NoError(t, err, "get index")
59-
defer resp.Body.Close()
60-
data, _ := io.ReadAll(resp.Body)
61-
require.Equal(t, string(data), "index-test-file")
80+
req, err := http.NewRequestWithContext(context.Background(), "GET", path, nil)
81+
require.NoError(t, err)
82+
resp, err := http.DefaultClient.Do(req)
83+
require.NoError(t, err, "get index")
84+
defer resp.Body.Close()
85+
data, _ := io.ReadAll(resp.Body)
86+
require.Equal(t, string(data), testCase.expected)
87+
}
6288
}
6389

64-
func TestCacheHeaderseAreCorrect(t *testing.T) {
90+
func TestCacheHeadersAreCorrect(t *testing.T) {
6591
rootFS := fstest.MapFS{
6692
"index.html": &fstest.MapFile{
6793
Data: []byte("index-test-file"),
@@ -120,7 +146,26 @@ func TestCacheHeaderseAreCorrect(t *testing.T) {
120146
require.Emptyf(t, cache, "expected path %q to be un-cacheable", path)
121147
require.NoError(t, resp.Body.Close(), "closing response")
122148
}
149+
}
150+
151+
func TestReturnsErrorIfNoIndex(t *testing.T) {
152+
rootFS := fstest.MapFS{
153+
// No index.html - so our router will have no fallback!
154+
"favicon.ico": &fstest.MapFile{
155+
Data: []byte("favicon-bytes"),
156+
},
157+
"bundle.js": &fstest.MapFile{
158+
Data: []byte("bundle-js-bytes"),
159+
},
160+
"icon.svg": &fstest.MapFile{
161+
Data: []byte("svg-bytes"),
162+
},
163+
}
123164

165+
// When no index.html is available, the site handler should panic
166+
require.Panics(t, func() {
167+
site.Handler(rootFS, slog.Logger{}, defaultTemplateFunc)
168+
})
124169
}
125170

126171
func defaultTemplateFunc(r *http.Request) site.HtmlState {

0 commit comments

Comments
 (0)