fix(07): WR-01 NewRouter returns error instead of panicking on bad static FS
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fbda7cbe5e
commit
b61f36f17e
8 changed files with 55 additions and 14 deletions
|
|
@ -122,7 +122,11 @@ func main() {
|
||||||
fileDeps := web.FilesDeps{Queries: q, Files: filesStore, MaxUploadMB: maxUploadMB}
|
fileDeps := web.FilesDeps{Queries: q, Files: filesStore, MaxUploadMB: maxUploadMB}
|
||||||
|
|
||||||
// D-09: pass the embedded static FS — binary has zero runtime file dependencies.
|
// D-09: pass the embedded static FS — binary has zero runtime file dependencies.
|
||||||
router := web.NewRouter(pool, assets.Static, deps, tabloDeps, taskDeps, fileDeps, csrfKey, env)
|
router, err := web.NewRouter(pool, assets.Static, deps, tabloDeps, taskDeps, fileDeps, csrfKey, env)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("router init failed", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@ func newTestRouterWithCSRF(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||||
csrfKey[i] = byte(i + 1)
|
csrfKey[i] = byte(i + 1)
|
||||||
}
|
}
|
||||||
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||||
return NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, FilesDeps{Queries: q}, csrfKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, FilesDeps{Queries: q}, csrfKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newTestRouterWithCSRF: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractCSRFToken performs a GET request and extracts the _csrf token from the
|
// extractCSRFToken performs a GET request and extracts the _csrf token from the
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,22 @@ var testCSRFKey = func() []byte {
|
||||||
// Referer header are accepted.
|
// Referer header are accepted.
|
||||||
func newTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
func newTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||||
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||||
return NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newTestRouter: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTestRouterWithLimiter builds a router with an injected LimiterStore,
|
// newTestRouterWithLimiter builds a router with an injected LimiterStore,
|
||||||
// enabling rate-limit tests to use a fake clock.
|
// enabling rate-limit tests to use a fake clock.
|
||||||
func newTestRouterWithLimiter(q *sqlc.Queries, store *auth.Store, rl *auth.LimiterStore) http.Handler {
|
func newTestRouterWithLimiter(q *sqlc.Queries, store *auth.Store, rl *auth.LimiterStore) http.Handler {
|
||||||
deps := AuthDeps{Queries: q, Store: store, Secure: false, Limiter: rl}
|
deps := AuthDeps{Queries: q, Store: store, Secure: false, Limiter: rl}
|
||||||
return NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newTestRouterWithLimiter: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCSRFToken performs a GET request to path and extracts the CSRF token
|
// getCSRFToken performs a GET request to path and extracts the CSRF token
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,11 @@ func newFileTestRouter(q *sqlc.Queries, store *auth.Store, fileStore files.FileS
|
||||||
tabloDeps := TablosDeps{Queries: q}
|
tabloDeps := TablosDeps{Queries: q}
|
||||||
taskDeps := TasksDeps{Queries: q}
|
taskDeps := TasksDeps{Queries: q}
|
||||||
fileDeps := FilesDeps{Queries: q, Files: fileStore, MaxUploadMB: 25}
|
fileDeps := FilesDeps{Queries: q, Files: fileStore, MaxUploadMB: 25}
|
||||||
return NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, fileDeps, testCSRFKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, fileDeps, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newFileTestRouter: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- TestFileUpload (FILE-01, FILE-02) ----
|
// ---- TestFileUpload (FILE-01, FILE-02) ----
|
||||||
|
|
@ -165,7 +169,10 @@ func TestFileUploadTooLarge(t *testing.T) {
|
||||||
tabloDeps := TablosDeps{Queries: q}
|
tabloDeps := TablosDeps{Queries: q}
|
||||||
taskDeps := TasksDeps{Queries: q}
|
taskDeps := TasksDeps{Queries: q}
|
||||||
fileDeps := FilesDeps{Queries: q, Files: stub, MaxUploadMB: 1}
|
fileDeps := FilesDeps{Queries: q, Files: stub, MaxUploadMB: 1}
|
||||||
router := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, fileDeps, testCSRFKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, fileDeps, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRouter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
user := preInsertUser(t, ctx, q, "filelarge@example.com", "correct-horse-12")
|
user := preInsertUser(t, ctx, q, "filelarge@example.com", "correct-horse-12")
|
||||||
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,11 @@ import (
|
||||||
func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||||
tabloDeps := TablosDeps{Queries: q}
|
tabloDeps := TablosDeps{Queries: q}
|
||||||
return NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newTabloTestRouter: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginUser signs up a user and returns the session cookie set after signup.
|
// loginUser signs up a user and returns the session cookie set after signup.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,11 @@ func newTaskTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||||
tabloDeps := TablosDeps{Queries: q}
|
tabloDeps := TablosDeps{Queries: q}
|
||||||
taskDeps := TasksDeps{Queries: q}
|
taskDeps := TasksDeps{Queries: q}
|
||||||
return NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
panic("newTaskTestRouter: " + err.Error())
|
||||||
|
}
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- TestTasksKanbanRenders (TASK-01) ----
|
// ---- TestTasksKanbanRenders (TASK-01) ----
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,10 @@ func TestReadyz_Down(t *testing.T) {
|
||||||
// was public. The HTMX demo content is tested by
|
// was public. The HTMX demo content is tested by
|
||||||
// TestProtected_HomeAuthRendersUserEmail in handlers_auth_test.go.
|
// TestProtected_HomeAuthRendersUserEmail in handlers_auth_test.go.
|
||||||
func TestIndex_UnauthRedirects(t *testing.T) {
|
func TestIndex_UnauthRedirects(t *testing.T) {
|
||||||
router := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, FilesDeps{}, testCSRFKey, "dev")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRouter: %v", err)
|
||||||
|
}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
|
||||||
|
|
@ -107,7 +110,10 @@ func TestIndex_UnauthRedirects(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDemoTime_Fragment(t *testing.T) {
|
func TestDemoTime_Fragment(t *testing.T) {
|
||||||
router := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, FilesDeps{}, testCSRFKey, "dev")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRouter: %v", err)
|
||||||
|
}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/demo/time", nil)
|
req := httptest.NewRequest(http.MethodGet, "/demo/time", nil)
|
||||||
|
|
||||||
|
|
@ -130,7 +136,10 @@ func TestDemoTime_Fragment(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestID_HeaderSet(t *testing.T) {
|
func TestRequestID_HeaderSet(t *testing.T) {
|
||||||
router := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, FilesDeps{}, testCSRFKey, "dev")
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), AuthDeps{}, TablosDeps{}, TasksDeps{}, FilesDeps{}, testCSRFKey, "dev", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRouter: %v", err)
|
||||||
|
}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -45,7 +46,7 @@ type Pinger interface {
|
||||||
// trustedOrigins is an optional list of additional origins for the CSRF
|
// trustedOrigins is an optional list of additional origins for the CSRF
|
||||||
// referer check (used in integration tests to allow localhost requests without
|
// referer check (used in integration tests to allow localhost requests without
|
||||||
// a Referer header). In production, pass no extra args — leave empty.
|
// a Referer header). In production, pass no extra args — leave empty.
|
||||||
func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDeps, taskDeps TasksDeps, fileDeps FilesDeps, csrfKey []byte, env string, trustedOrigins ...string) http.Handler {
|
func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDeps, taskDeps TasksDeps, fileDeps FilesDeps, csrfKey []byte, env string, trustedOrigins ...string) (http.Handler, error) {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(RequestIDMiddleware)
|
r.Use(RequestIDMiddleware)
|
||||||
r.Use(chimw.RealIP)
|
r.Use(chimw.RealIP)
|
||||||
|
|
@ -126,10 +127,10 @@ func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDep
|
||||||
// the FS. The "fs" local name is avoided to prevent shadowing the "io/fs" import.
|
// the FS. The "fs" local name is avoided to prevent shadowing the "io/fs" import.
|
||||||
sub, err := fs.Sub(staticFS, "static")
|
sub, err := fs.Sub(staticFS, "static")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("router: failed to sub static FS: " + err.Error())
|
return nil, fmt.Errorf("router: failed to sub static FS: %w", err)
|
||||||
}
|
}
|
||||||
fileHandler := http.StripPrefix("/static/", http.FileServer(http.FS(sub)))
|
fileHandler := http.StripPrefix("/static/", http.FileServer(http.FS(sub)))
|
||||||
r.Get("/static/*", fileHandler.ServeHTTP)
|
r.Get("/static/*", fileHandler.ServeHTTP)
|
||||||
|
|
||||||
return r
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue