feat(03-03): add typed path params and per-plugin rollback

Compile regex and enum constraints at route registration so malformed and unknown IDs share a 404, and named rollback errors isolate one plugin's history.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-17 20:19:59 +02:00
parent a9c3e7690f
commit 8e3bf266d8
10 changed files with 354 additions and 58 deletions

View File

@@ -109,28 +109,17 @@ func TestCORSPreflightBypassesNamedAuth(t *testing.T) {
}
}
func TestIntParamMalformedIsFalse(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/items/abc", nil)
req.SetPathValue("id", "abc")
if _, ok := IntParam(req, "id"); ok {
t.Fatal("malformed id must be false")
}
req.SetPathValue("id", "12")
n, ok := IntParam(req, "id")
if !ok || n != 12 {
t.Fatalf("got %d %t", n, ok)
}
}
func TestTypedIDRouteReturns404(t *testing.T) {
r := New(nil)
r.Get("/items/{id}", func(w http.ResponseWriter, req *http.Request) {
if _, ok := IntParam(req, "id"); !ok {
id, ok := IntParam(req, "id")
if !ok || id != 1 {
http.NotFound(w, req)
return
}
w.WriteHeader(http.StatusOK)
})
r.Where("id", `[0-9]+`)
h, err := r.compile()
if err != nil {
t.Fatal(err)
@@ -138,6 +127,56 @@ func TestTypedIDRouteReturns404(t *testing.T) {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items/nope", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("malformed status = %d", rec.Code)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items/99", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("unknown status = %d", rec.Code)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items/1", nil))
if rec.Code != http.StatusOK {
t.Fatalf("known status = %d", rec.Code)
}
}
func TestWhereInRejectsOutsideEnum(t *testing.T) {
r := New(nil)
r.Get("/kinds/{kind}", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.WhereIn("kind", "widget", "gadget")
h, err := r.compile()
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/kinds/other", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d", rec.Code)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/kinds/widget", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
}
func TestWhereInvalidRegexFailsCompile(t *testing.T) {
r := New(nil)
r.Get("/items/{id}", func(http.ResponseWriter, *http.Request) {})
r.Where("id", "[")
if _, err := r.compile(); err == nil {
t.Fatal("want compile error for invalid regex")
}
}
func TestWhereUnknownParamFailsCompile(t *testing.T) {
r := New(nil)
r.Get("/items/{id}", func(http.ResponseWriter, *http.Request) {})
r.Where("slug", `[a-z]+`)
if _, err := r.compile(); err == nil || !strings.Contains(err.Error(), "slug") {
t.Fatal("want unknown param error")
}
}