Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,9 @@ func New(addr string, store Store, encKey []byte, notifier *notify.Notifier, ini
// SPA catch-all: serve index.html for all frontend routes
mux.HandleFunc("GET /login", s.handleSPA)
mux.HandleFunc("GET /register", s.handleSPA)
mux.HandleFunc("GET /forgot-password", s.handleSPA)
mux.HandleFunc("GET /users", s.handleSPA)
mux.HandleFunc("GET /agents", s.handleSPA)
mux.HandleFunc("GET /vaults/{$}", s.handleSPA)
mux.HandleFunc("GET /vaults/{name...}", s.handleSPA)
mux.HandleFunc("GET /invite/{token...}", s.handleSPA)
Expand Down
47 changes: 47 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5592,3 +5592,50 @@ func TestServiceRemoveUnauthenticated(t *testing.T) {
t.Fatalf("expected 401, got %d: %s", rec.Code, rec.Body.String())
}
}

// TestSPACatchAllRoutes locks the SPA fallback contract: every top-level
// frontend route must serve index.html so a hard refresh doesn't 404.
func TestSPACatchAllRoutes(t *testing.T) {
srv := newTestServer()

// /invite/abc is not tested here. Two routes could match it: the
// API handler at GET /invite/{token} and the SPA fallback at
// GET /invite/{token...}. Go's ServeMux prefers the single-segment
// pattern, so the request hits handleInviteRedeem (which 404s
// because the mock store has no invite) instead of handleSPA — a
// 404 here would not mean the SPA route is broken.
paths := []string{
"/",
"/login",
"/register",
"/forgot-password",
"/users",
"/agents",
"/change-password",
"/oauth/callback",
"/account/settings",
"/manage/settings",
"/vaults/",
"/vaults/default",
"/approve/1",
}
for _, p := range paths {
t.Run(p, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, p, nil)
rec := httptest.NewRecorder()
srv.httpServer.Handler.ServeHTTP(rec, req)
if rec.Code == http.StatusNotFound {
t.Fatalf("expected non-404 for SPA route, got %d: %s", rec.Code, rec.Body.String())
}
})
}

t.Run("unknown path stays 404", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/definitely-not-a-route", nil)
rec := httptest.NewRecorder()
srv.httpServer.Handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 for unregistered path, got %d: %s", rec.Code, rec.Body.String())
}
})
}