package httpx import ( "context" "net/http" "net/http/httptest" "testing" "bountyboard/internal/domain" ) func userCtx(r *http.Request, u *domain.User) *http.Request { return r.WithContext(context.WithValue(r.Context(), ctxKeyUser, u)) } // TestRBACMatrix exercises requireRole for every role/requirement combination. func TestRBACMatrix(t *testing.T) { s := newTestServer(t) roles := func(admin, consultant, developer bool) domain.Roles { return domain.Roles{Admin: admin, Consultant: consultant, Developer: developer} } tests := []struct { name string user *domain.User required string wantCode int }{ {"admin passes admin gate", &domain.User{Roles: roles(true, false, false)}, "admin", http.StatusOK}, {"developer blocked from admin gate", &domain.User{Roles: roles(false, false, true)}, "admin", http.StatusForbidden}, {"consultant blocked from admin gate", &domain.User{Roles: roles(false, true, false)}, "admin", http.StatusForbidden}, {"consultant passes consultant gate", &domain.User{Roles: roles(false, true, false)}, "consultant", http.StatusOK}, {"developer blocked from consultant gate", &domain.User{Roles: roles(false, false, true)}, "consultant", http.StatusForbidden}, {"admin blocked from consultant gate without flag", &domain.User{Roles: roles(true, false, false)}, "consultant", http.StatusForbidden}, {"developer passes developer gate", &domain.User{Roles: roles(false, false, true)}, "developer", http.StatusOK}, {"multi-role user passes both gates", &domain.User{Roles: roles(false, true, true)}, "developer", http.StatusOK}, {"no user in context", nil, "developer", http.StatusForbidden}, {"unknown role never passes", &domain.User{Roles: roles(true, true, true)}, "superuser", http.StatusForbidden}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := s.requireRole(tt.required, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) r := httptest.NewRequest(http.MethodGet, "/probe", nil) if tt.user != nil { r = userCtx(r, tt.user) } rec := httptest.NewRecorder() h.ServeHTTP(rec, r) if rec.Code != tt.wantCode { t.Errorf("code = %d, want %d", rec.Code, tt.wantCode) } }) } } func TestCSRFDoubleSubmit(t *testing.T) { s := newTestServer(t) ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) tests := []struct { name string method string cookie string header string wantCode int }{ {"GET passes without token", http.MethodGet, "", "", http.StatusOK}, {"HEAD passes without token", http.MethodHead, "", "", http.StatusOK}, {"POST without anything blocked", http.MethodPost, "", "", http.StatusForbidden}, {"POST with matching pair passes", http.MethodPost, "tok123", "tok123", http.StatusOK}, {"POST with mismatched header blocked", http.MethodPost, "tok123", "other", http.StatusForbidden}, {"POST with header but no cookie blocked", http.MethodPost, "", "tok123", http.StatusForbidden}, {"POST with cookie but no header blocked", http.MethodPost, "tok123", "", http.StatusForbidden}, {"DELETE requires token", http.MethodDelete, "tok123", "tok123", http.StatusOK}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(tt.method, "/probe", nil) if tt.cookie != "" { r.AddCookie(&http.Cookie{Name: csrfCookie, Value: tt.cookie}) } if tt.header != "" { r.Header.Set(csrfHeader, tt.header) } rec := httptest.NewRecorder() s.requireCSRF(ok).ServeHTTP(rec, r) if rec.Code != tt.wantCode { t.Errorf("code = %d, want %d", rec.Code, tt.wantCode) } }) } }