| 1 | package calendartest |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "net/url" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // The scheduled-tick route must be mounted OUTSIDE csrf.Protect, and |
| 12 | // this test is the whole reason to believe it is. |
| 13 | // |
| 14 | // csrf.Protect refuses any POST that carries no Sec-Fetch-Site, Origin |
| 15 | // or Referer. The platform's scheduler is not a browser and sends none |
| 16 | // of the three, so a tick route mounted inside the protected router |
| 17 | // answers 403 — and the platform reads 4xx as "do not retry". The |
| 18 | // result is reminders that never fire, on an app that looks entirely |
| 19 | // healthy, with nothing in any log to say why. |
| 20 | // |
| 21 | // So: a POST with none of the CSRF evidence must NOT be refused by |
| 22 | // CSRF. It is refused by carlos.Tick instead, which is a different |
| 23 | // refusal for a different reason — and off-platform, with no |
| 24 | // $CARLOS_ADMIN_TOKEN in the environment, Tick is false, so 403 here is |
| 25 | // the correct answer arrived at correctly. |
| 26 | // |
| 27 | // The two are told apart by the body: csrf.Protect writes its own, and |
| 28 | // this handler writes "forbidden". |
| 29 | func TestTheTickRouteIsNotBehindCSRF(t *testing.T) { |
| 30 | h := newApp(t).Handler() |
| 31 | |
| 32 | // Deliberately bare: no Origin, no Referer, no Sec-Fetch-Site — |
| 33 | // exactly what the platform's scheduler sends. |
| 34 | req := httptest.NewRequest(http.MethodPost, "/jobs/reminders", nil) |
| 35 | rec := httptest.NewRecorder() |
| 36 | h.ServeHTTP(rec, req) |
| 37 | |
| 38 | if rec.Code != http.StatusForbidden { |
| 39 | t.Fatalf("POST /jobs/reminders: status %d, want 403 from the tick guard", rec.Code) |
| 40 | } |
| 41 | if got := strings.TrimSpace(rec.Body.String()); got != "forbidden" { |
| 42 | t.Errorf("POST /jobs/reminders was refused by %q, which is not the tick guard — "+ |
| 43 | "it is almost certainly csrf.Protect, which means the route is inside the "+ |
| 44 | "protected router and ticks will 403 forever", got) |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // The contrast case: an ordinary state-changing route IS behind CSRF, |
| 49 | // so the test above is proving a difference rather than proving that |
| 50 | // CSRF is off everywhere. |
| 51 | func TestOrdinaryPostsAreBehindCSRF(t *testing.T) { |
| 52 | h := newApp(t).Handler() |
| 53 | |
| 54 | req := httptest.NewRequest(http.MethodPost, "/signin", |
| 55 | strings.NewReader(url.Values{"address": {"someone@example.com"}}.Encode())) |
| 56 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| 57 | // No Origin: a cross-origin POST, which must be refused. |
| 58 | req.Header.Set("Sec-Fetch-Site", "cross-site") |
| 59 | rec := httptest.NewRecorder() |
| 60 | h.ServeHTTP(rec, req) |
| 61 | |
| 62 | if rec.Code == http.StatusSeeOther { |
| 63 | t.Error("a cross-site POST to /signin succeeded; csrf.Protect is not covering the app") |
| 64 | } |
| 65 | } |
| 66 | |