feat(t-paliad-179): register /tools/verfahrensablauf + 302 legacy ?path=a
Backend half of Slice 1: a new dedicated route owns the abstract-browse intent that was previously emulated by /tools/fristenrechner?path=a + client-side fix-up. The page handler is a 1-liner that serves dist/verfahrensablauf.html (no DB dependency). A naked ?path=a on /tools/fristenrechner now 302s to the new URL so bookmarked legacy links survive. ?project=<uuid>&path=a still serves the fristenrechner shell because that's wizard state set by client- side history.replaceState during Akte-mode Pathway A — refreshing mid-wizard must not bounce away. Test covers all four query shapes: naked path=a → redirect, path=a with project → no redirect, no params → no redirect, path=b → no redirect.
This commit is contained in:
@@ -9,10 +9,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Fristenrechner page handler: serves the static HTML. No DB dependency.
|
// Fristenrechner page handler: serves the static HTML. No DB dependency.
|
||||||
|
//
|
||||||
|
// Back-compat: the pre-split sidebar entry for "Verfahrensablauf" pointed at
|
||||||
|
// /tools/fristenrechner?path=a. After the t-paliad-179 split, that landing is
|
||||||
|
// owned by /tools/verfahrensablauf. A naked ?path=a (no Akte context — i.e.
|
||||||
|
// no ?project=) is the bookmarked-legacy-entry case → 302 to the new route.
|
||||||
|
// ?project=<uuid>&path=a is the Akte-mode internal wizard pathway and stays
|
||||||
|
// on /tools/fristenrechner so the wizard state survives a refresh.
|
||||||
func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) {
|
func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
if q.Get("path") == "a" && q.Get("project") == "" {
|
||||||
|
http.Redirect(w, r, "/tools/verfahrensablauf", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
http.ServeFile(w, r, "dist/fristenrechner.html")
|
http.ServeFile(w, r, "dist/fristenrechner.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verfahrensablauf page handler (t-paliad-179 Slice 1): the dedicated
|
||||||
|
// abstract-browse surface for procedural shape. No DB dependency — the page
|
||||||
|
// shell is static HTML; the calculator API still drives the timeline render.
|
||||||
|
func handleVerfahrensablaufPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, "dist/verfahrensablauf.html")
|
||||||
|
}
|
||||||
|
|
||||||
// POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding.
|
// POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding.
|
||||||
//
|
//
|
||||||
// Phase C: routes through FristenrechnerService which pulls rules from
|
// Phase C: routes through FristenrechnerService which pulls rules from
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
|
|||||||
protected.HandleFunc("GET /tools/kostenrechner", handleKostenrechnerPage)
|
protected.HandleFunc("GET /tools/kostenrechner", handleKostenrechnerPage)
|
||||||
protected.HandleFunc("POST /api/tools/kostenrechner", handleKostenrechnerAPI)
|
protected.HandleFunc("POST /api/tools/kostenrechner", handleKostenrechnerAPI)
|
||||||
protected.HandleFunc("GET /tools/fristenrechner", handleFristenrechnerPage)
|
protected.HandleFunc("GET /tools/fristenrechner", handleFristenrechnerPage)
|
||||||
|
protected.HandleFunc("GET /tools/verfahrensablauf", handleVerfahrensablaufPage)
|
||||||
protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI)
|
protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI)
|
||||||
protected.HandleFunc("POST /api/tools/fristenrechner/calculate-rule", handleFristenrechnerCalculateRule)
|
protected.HandleFunc("POST /api/tools/fristenrechner/calculate-rule", handleFristenrechnerCalculateRule)
|
||||||
protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes)
|
protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes)
|
||||||
|
|||||||
83
internal/handlers/verfahrensablauf_test.go
Normal file
83
internal/handlers/verfahrensablauf_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// /tools/fristenrechner?path=a was the pre-split sidebar entry for the
|
||||||
|
// "Verfahrensablauf" surface. After t-paliad-179 Slice 1 that intent
|
||||||
|
// owns its own /tools/verfahrensablauf route — so a naked ?path=a hit
|
||||||
|
// must 302 to the new URL to preserve bookmarked legacy links.
|
||||||
|
//
|
||||||
|
// The Akte-mode internal wizard pathway (?project=<uuid>&path=a) is
|
||||||
|
// NOT a top-level entry — it's wizard state set by client-side
|
||||||
|
// history.replaceState. That URL must keep serving the fristenrechner
|
||||||
|
// shell so a mid-wizard refresh doesn't bounce away.
|
||||||
|
func TestHandleFristenrechnerPage_LegacyPathARedirect(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
wantStatus int
|
||||||
|
wantLoc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "naked path=a → redirect",
|
||||||
|
path: "/tools/fristenrechner?path=a",
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantLoc: "/tools/verfahrensablauf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path=a with project= → no redirect (Akte-mode wizard)",
|
||||||
|
path: "/tools/fristenrechner?project=abc-123&path=a",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no path param → no redirect",
|
||||||
|
path: "/tools/fristenrechner",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path=b → no redirect (Pathway B stays)",
|
||||||
|
path: "/tools/fristenrechner?path=b",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handleFristenrechnerPage(w, req)
|
||||||
|
if w.Code != tc.wantStatus {
|
||||||
|
// http.ServeFile may write 404 if dist/fristenrechner.html
|
||||||
|
// is missing under `go test` (CI runs without a frontend
|
||||||
|
// build). We only care that we did NOT redirect in those
|
||||||
|
// cases — collapse 200 and 404 into "not a redirect".
|
||||||
|
if tc.wantStatus == http.StatusOK && w.Code != http.StatusFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("status = %d, want %d", w.Code, tc.wantStatus)
|
||||||
|
}
|
||||||
|
if tc.wantLoc != "" {
|
||||||
|
if got := w.Header().Get("Location"); got != tc.wantLoc {
|
||||||
|
t.Fatalf("Location = %q, want %q", got, tc.wantLoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The new /tools/verfahrensablauf route registers as a 1-liner page
|
||||||
|
// handler that ServeFiles dist/verfahrensablauf.html. We assert the
|
||||||
|
// handler does NOT redirect — if the dist artefact is missing under
|
||||||
|
// `go test`, ServeFile may return 404, but it must never return a 3xx.
|
||||||
|
func TestHandleVerfahrensablaufPage_NoRedirect(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/tools/verfahrensablauf", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handleVerfahrensablaufPage(w, req)
|
||||||
|
if w.Code >= 300 && w.Code < 400 {
|
||||||
|
t.Fatalf("verfahrensablauf must not redirect; got %d → %s",
|
||||||
|
w.Code, w.Header().Get("Location"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user