Add cache control to Files API static assets

Brand assets served at /api/files/v1/static had no cache headers.
Introduce brand.Assets to own the embedded filesystem, content-hash
ETags, and HTTP serving. Responses now carry Cache-Control and ETag
so clients can cache and revalidate; stable email URLs stay
revalidatable (max-age=3600, no immutable).

Replace hardcoded Default*Path constants with StaticPathPrefix,
logical filename constants, and StaticPath(). NewAssets validates
required assets at startup so a rename fails fast instead of 404ing
in sent emails. The files handler keeps routing and 404 rendering;
ServeAssets sets cache headers and serves the file.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-06-12 12:14:40 +02:00
parent c33540637d
commit dbfd191bc5
8 changed files with 208 additions and 68 deletions

View File

@@ -19,10 +19,12 @@ import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/securecookie"
)
@@ -67,6 +69,43 @@ func TestHandleGetFile_InvalidGID(t *testing.T) {
assert.Equal(t, http.StatusNotFound, rec.Code)
}
func TestHandleGetStaticFile(t *testing.T) {
t.Parallel()
mux := NewMux(
log.NewLogger(log.WithOutput(io.Discard)),
nil,
nil,
nil,
securecookie.Config{},
"test-secret",
)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/static/probo.png", nil)
mux.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Header().Get("Cache-Control"), "max-age=3600")
etag := rec.Header().Get("ETag")
require.NotEmpty(t, etag)
require.True(t, strings.HasPrefix(etag, `"`) && strings.HasSuffix(etag, `"`))
recNotModified := httptest.NewRecorder()
reqNotModified := httptest.NewRequest(http.MethodGet, "/static/probo.png", nil)
reqNotModified.Header.Set("If-None-Match", etag)
mux.ServeHTTP(recNotModified, reqNotModified)
require.Equal(t, http.StatusNotModified, recNotModified.Code)
recMissing := httptest.NewRecorder()
reqMissing := httptest.NewRequest(http.MethodGet, "/static/does-not-exist.png", nil)
mux.ServeHTTP(recMissing, reqMissing)
require.Equal(t, http.StatusNotFound, recMissing.Code)
}
func TestHandleGetFile_UnauthenticatedReturns401(t *testing.T) {
t.Parallel()