managarten/services/mana-sync/internal/auth/jwt_test.go
Till JS 4ff3ceb01a harden(mana-sync): fix WebSocket auth, add validation, tests, and docs
Critical security and correctness fixes for the sync server:

Security:
- Fix WebSocket JWT validation — was completely broken (hardcoded
  "pending-auth"). Now validates JWT via JWKS, rejects invalid tokens,
  enforces 10-second auth deadline, sends auth-ok confirmation.
- Add 10 MB request body size limit (prevents OOM attacks)
- Validate op field (must be insert/update/delete)
- Validate table and id fields (must be non-empty)
- Abort sync on RecordChange failure (was silently continuing)

Correctness:
- Fix silent JSON unmarshal errors in store (now returns error)
- Copy client set before iterating in NotifyUser (prevents race)
- Add write timeout on WebSocket notifications

Testing (19 tests, 0 -> 100% for unit-testable code):
- auth: token extraction, validator init, missing auth handling
- config: defaults, env override, invalid port
- sync: op validation, changeset validation, response format,
  field change round-trip, body size constant

Documentation:
- Add CLAUDE.md with architecture, sync protocol, LWW explanation,
  API endpoints, configuration, security notes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 02:41:56 +01:00

82 lines
2 KiB
Go

package auth
import (
"net/http"
"testing"
)
func TestExtractToken(t *testing.T) {
tests := []struct {
name string
header string
wantToken string
}{
{"valid bearer", "Bearer eyJhbGciOiJFZERTQSJ9.test.sig", "eyJhbGciOiJFZERTQSJ9.test.sig"},
{"missing bearer prefix", "eyJhbGciOiJFZERTQSJ9.test.sig", ""},
{"empty header", "", ""},
{"lowercase bearer", "bearer token123", ""},
{"only bearer", "Bearer ", ""},
{"bearer with space", "Bearer token123", " token123"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
if tt.header != "" {
r.Header.Set("Authorization", tt.header)
}
got := ExtractToken(r)
if got != tt.wantToken {
t.Errorf("ExtractToken() = %q, want %q", got, tt.wantToken)
}
})
}
}
func TestNewValidator(t *testing.T) {
v := NewValidator("http://localhost:3001/api/auth/jwks")
if v.jwksURL != "http://localhost:3001/api/auth/jwks" {
t.Errorf("jwksURL = %q, want 'http://localhost:3001/api/auth/jwks'", v.jwksURL)
}
if len(v.keys) != 0 {
t.Errorf("expected empty keys map, got %d keys", len(v.keys))
}
if v.fetchEvery.Minutes() != 5 {
t.Errorf("fetchEvery = %v, want 5m", v.fetchEvery)
}
}
func TestValidateTokenNoKeys(t *testing.T) {
// Validator with unreachable JWKS endpoint
v := NewValidator("http://localhost:99999/jwks")
_, err := v.ValidateToken("some.invalid.token")
if err == nil {
t.Error("expected error for token with no keys, got nil")
}
}
func TestUserIDFromRequestNoAuth(t *testing.T) {
v := NewValidator("http://localhost:99999/jwks")
r, _ := http.NewRequest("GET", "/", nil)
_, err := v.UserIDFromRequest(r)
if err == nil {
t.Error("expected error for request without auth header")
}
}
func TestUserIDFromRequestEmptyBearer(t *testing.T) {
v := NewValidator("http://localhost:99999/jwks")
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer ")
_, err := v.UserIDFromRequest(r)
if err == nil {
t.Error("expected error for empty bearer token")
}
}