diff --git a/README.md b/README.md index 2c657cc..8a3ad14 100644 --- a/README.md +++ b/README.md @@ -2721,7 +2721,7 @@ abuse limit later; they are tracked as future work. | ------ | --------------------------- | ----------- | | `GET` | `/` | Root redirect, 303 (authenticated → `/sources`, unauthenticated → `/pages/login`) | | `GET` | `/.well-known/healthcheck` | Health check (JSON: `status`, `now`, `uptimeSeconds`, `uptimeHuman`, `version`, `appname`, `maintenanceMode`) | -| `GET`, `HEAD` | `/s/*` | Static file serving (embedded CSS, JS). `GET` and `HEAD` only — every other method is answered `405 Method Not Allowed` with `Allow: GET, HEAD`. Pinned by `TestStaticServesOnlyGetAndHead` | +| `GET`, `HEAD` | `/s/*` | Static file serving (embedded CSS, JS). `GET` and `HEAD` only — `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `TRACE` and `CONNECT` are answered `405 Method Not Allowed` with `Allow: GET, HEAD`. Any other method (such as `PROPFIND`) is refused by chi before it reaches this route, and gets `405` without an `Allow` header. Pinned by `TestStaticServesOnlyGetAndHead` | | `POST` | `/webhook/{uuid}` | Webhook receiver endpoint. `POST` only — every other method is answered `405 Method Not Allowed` with `Allow: POST`. Rate limited (see [Rate Limiting](#rate-limiting)) | #### Authentication Endpoints diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go index 081fdef..429a7f7 100644 --- a/internal/server/routes_test.go +++ b/internal/server/routes_test.go @@ -397,9 +397,12 @@ func (e *testEnv) storedHash(t *testing.T, username string) string { // --- /s static group --- // TestStaticServesOnlyGetAndHead pins the methods the static group -// answers: GET and HEAD are served the asset, and any other method -// is refused with 405 and an Allow header naming those two. The -// README documents this; the test is what keeps the two from +// answers: GET and HEAD are served the asset, and the other methods +// chi routes (POST, PUT, DELETE and the rest) are refused with 405 +// and an Allow header naming those two. A method chi does not route, +// such as PROPFIND, is refused with 405 by the top-level router +// before it reaches the static group, so it gets no Allow header. +// The README documents this; the test is what keeps the two from // drifting. func TestStaticServesOnlyGetAndHead(t *testing.T) { t.Parallel() @@ -416,6 +419,7 @@ func TestStaticServesOnlyGetAndHead(t *testing.T) { http.MethodPost, http.MethodPut, http.MethodDelete, + "PROPFIND", } { t.Run(method, func(t *testing.T) { t.Parallel() @@ -436,6 +440,17 @@ func TestStaticServesOnlyGetAndHead(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Empty(t, w.Body.Bytes(), "HEAD must not carry a body") + case "PROPFIND": + assert.Equal( + t, http.StatusMethodNotAllowed, w.Code, + ) + assert.Empty(t, w.Header().Get("Allow"), + "chi refuses a method it does not route "+ + "before the static group runs") + assert.NotContains( + t, w.Body.String(), string(body), + "a refused method must not get the asset", + ) default: assert.Equal( t, http.StatusMethodNotAllowed, w.Code,