Files
BountyBoard/internal/http/authmw_test.go
T
etalon c1cc781279 feat: authentication with local accounts, sessions, CSRF, RBAC, and OIDC PKCE (phase 3)
- argon2id (t=3, m=64MiB, p=2) PHC hashing honoring embedded params
- token-bucket rate limiting (10/15min per IP+email) on login/register
- opaque 32B session tokens in Mongo, 30-day sliding expiry, logout-all
- CSRF double-submit cookie/header on authenticated mutations
- bootstrap admin from env with forced first-login password change
- requireAuth/requireRole middleware with disabled-account enforcement
- OIDC code flow with PKCE: lazy discovery, account linking only on
  verified email, auto-created developer accounts
- unit tests (RBAC matrix, CSRF, password, rate limiter) + integration
  suite covering the full auth matrix incl. an in-test fake IdP

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:31:25 +02:00

94 lines
3.6 KiB
Go

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)
}
})
}
}