Verify shared route boundaries and signout state
1 file changed,
+72
−0
connect/routes_test.go+72 −0
CI passed — run details
diff --git a/connect/routes_test.go b/connect/routes_test.go| new file mode 100644 |
| index 0000000..4fe0ba5 |
| --- /dev/null |
| +++ b/connect/routes_test.go |
| @@ -0,0 +1,72 @@ |
| +package connect |
| + |
| +import ( |
| + "amadan.net/oficina/home/handoff" |
| + "net/http" |
| + "net/http/httptest" |
| + "testing" |
| +) |
| + |
| +func TestRouteGuardExemptsOnlyCallbackAndPreservesPolicy(t *testing.T) { |
| + b := &Bridge{render: func(w http.ResponseWriter, r *http.Request, state, label string, status int) { w.WriteHeader(status) }} |
| + h := &Routes{bridge: b, cfg: RoutesConfig{Origin: "https://slides.example.com", Issuer: "https://home.example.com", SessionCookie: "__Host-session", FormPolicy: "default-src 'none'; form-action 'self' https://home.example.com", Protect: func(next http.Handler) http.Handler { |
| + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + if r.Method == "POST" { |
| + w.WriteHeader(403) |
| + return |
| + } |
| + next.ServeHTTP(w, r) |
| + }) |
| + }}} |
| + called := 0 |
| + handler := h.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called++; w.WriteHeader(204) })) |
| + for _, tc := range []struct { |
| + method, origin, path string |
| + dup bool |
| + want int |
| + }{{"POST", h.cfg.Origin, handoff.CallbackPath, false, 204}, {"POST", h.cfg.Origin, handoff.CallbackPath + "/", false, 403}, {"POST", h.cfg.Origin, "/account/home", false, 403}, {"GET", "https://wrong.example.com", handoff.CallbackPath, false, 404}, {"GET", h.cfg.Origin, "/", true, 400}, {"GET", h.cfg.Origin, "/", false, 204}} { |
| + r := httptest.NewRequest(tc.method, tc.origin+tc.path, nil) |
| + if tc.dup { |
| + r.AddCookie(&http.Cookie{Name: h.cfg.SessionCookie, Value: "a"}) |
| + r.AddCookie(&http.Cookie{Name: h.cfg.SessionCookie, Value: "b"}) |
| + } |
| + w := httptest.NewRecorder() |
| + handler.ServeHTTP(w, r) |
| + if w.Code != tc.want { |
| + t.Fatalf("%s %s: %d", tc.method, tc.path, w.Code) |
| + } |
| + if w.Header().Get("Content-Security-Policy") != h.cfg.FormPolicy { |
| + t.Fatal("app policy replaced") |
| + } |
| + } |
| + if called != 2 { |
| + t.Fatal("guard bypass", called) |
| + } |
| + localCalls := 0 |
| + local := h.LocalGuard(func(w http.ResponseWriter, r *http.Request) { localCalls++ }) |
| + r := httptest.NewRequest("GET", h.cfg.Origin+"/auth/verify", nil) |
| + r.AddCookie(&http.Cookie{Name: "__Host-rastrillo_pending", Value: "a"}) |
| + r.AddCookie(&http.Cookie{Name: "__Host-rastrillo_pending", Value: "b"}) |
| + w := httptest.NewRecorder() |
| + local(w, r) |
| + if w.Code != 400 || localCalls != 0 { |
| + t.Fatal("ambiguous local pending reached plugin") |
| + } |
| +} |
| +func TestSignoutEndsLocallyAndClearsHandoffPointers(t *testing.T) { |
| + revoked := false |
| + h := &Routes{cfg: RoutesConfig{SignOut: func(http.ResponseWriter, *http.Request) { revoked = true }}} |
| + w := httptest.NewRecorder() |
| + h.Signout(w, httptest.NewRequest("POST", "https://slides.example.com/signout", nil)) |
| + if !revoked || w.Code != 303 || w.Header().Get("Location") != "/signed-out" { |
| + t.Fatal("signout restarted Home exchange") |
| + } |
| + if len(w.Result().Cookies()) != 6 { |
| + t.Fatal("handoff pointers left behind") |
| + } |
| + for _, c := range w.Result().Cookies() { |
| + if c.MaxAge != -1 || !c.Secure || !c.HttpOnly { |
| + t.Fatal("cookie not cleared safely") |
| + } |
| + } |
| +} |