diff options
| author | bndw <ben@bdw.to> | 2026-02-15 10:06:18 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-15 10:06:18 -0800 |
| commit | d744c32f1bc7411e04c97a9d14c172baaa0e4a89 (patch) | |
| tree | e9758c48d38798f2922ffbeaccd5f6105821a83a /internal/handler/websocket/handler_test.go | |
| parent | 8ae69fde76945377189281182954c946ff9ad419 (diff) | |
test: add integration tests for NIP-42 AUTH and rate limiting
Add comprehensive WebSocket handler integration tests that verify:
- NIP-42 authentication flow (auth required, challenge/response)
- Allowlist enforcement (reject unauthorized pubkeys)
- Rate limiting by IP address
- Rate limiting by authenticated pubkey
- No-auth mode works correctly
These tests use real WebSocket connections and would have caught
the AUTH timeout bug and other protocol issues.
Tests cover:
- TestAuthRequired: Verifies AUTH challenge sent, client authenticates, publish succeeds
- TestAuthNotInAllowlist: Verifies pubkeys not in allowlist are rejected
- TestRateLimitByIP: Verifies unauthenticated clients are rate limited by IP
- TestRateLimitByPubkey: Verifies authenticated clients are rate limited by pubkey
- TestNoAuthWhenDisabled: Verifies publishing works when auth is disabled
Diffstat (limited to 'internal/handler/websocket/handler_test.go')
| -rw-r--r-- | internal/handler/websocket/handler_test.go | 526 |
1 files changed, 526 insertions, 0 deletions
diff --git a/internal/handler/websocket/handler_test.go b/internal/handler/websocket/handler_test.go new file mode 100644 index 0000000..9f02510 --- /dev/null +++ b/internal/handler/websocket/handler_test.go | |||
| @@ -0,0 +1,526 @@ | |||
| 1 | package websocket | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "encoding/json" | ||
| 6 | "fmt" | ||
| 7 | "net/http/httptest" | ||
| 8 | "strings" | ||
| 9 | "sync" | ||
| 10 | "testing" | ||
| 11 | "time" | ||
| 12 | |||
| 13 | "northwest.io/muxstr/internal/ratelimit" | ||
| 14 | "northwest.io/muxstr/internal/storage" | ||
| 15 | "northwest.io/muxstr/internal/subscription" | ||
| 16 | ws "northwest.io/muxstr/internal/websocket" | ||
| 17 | pb "northwest.io/muxstr/api/nostr/v1" | ||
| 18 | |||
| 19 | "fiatjaf.com/nostr" | ||
| 20 | ) | ||
| 21 | |||
| 22 | // mockAuthStore implements the auth methods needed for testing | ||
| 23 | type mockAuthStore struct { | ||
| 24 | challenges map[string]time.Time | ||
| 25 | mu sync.Mutex | ||
| 26 | } | ||
| 27 | |||
| 28 | func newMockAuthStore() *mockAuthStore { | ||
| 29 | return &mockAuthStore{ | ||
| 30 | challenges: make(map[string]time.Time), | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | func (m *mockAuthStore) CreateAuthChallenge(ctx context.Context) (string, error) { | ||
| 35 | m.mu.Lock() | ||
| 36 | defer m.mu.Unlock() | ||
| 37 | challenge := fmt.Sprintf("test-challenge-%d", time.Now().UnixNano()) | ||
| 38 | m.challenges[challenge] = time.Now() | ||
| 39 | return challenge, nil | ||
| 40 | } | ||
| 41 | |||
| 42 | func (m *mockAuthStore) ValidateAndConsumeChallenge(ctx context.Context, challenge string) error { | ||
| 43 | m.mu.Lock() | ||
| 44 | defer m.mu.Unlock() | ||
| 45 | if _, exists := m.challenges[challenge]; !exists { | ||
| 46 | return fmt.Errorf("invalid challenge") | ||
| 47 | } | ||
| 48 | delete(m.challenges, challenge) | ||
| 49 | return nil | ||
| 50 | } | ||
| 51 | |||
| 52 | func (m *mockAuthStore) StoreEvent(ctx context.Context, event *storage.EventData) error { | ||
| 53 | return nil | ||
| 54 | } | ||
| 55 | |||
| 56 | func (m *mockAuthStore) QueryEvents(ctx context.Context, filters []*pb.Filter, opts *storage.QueryOptions) ([]*pb.Event, error) { | ||
| 57 | return []*pb.Event{}, nil | ||
| 58 | } | ||
| 59 | |||
| 60 | func (m *mockAuthStore) ProcessDeletion(ctx context.Context, event *pb.Event) error { | ||
| 61 | return nil | ||
| 62 | } | ||
| 63 | |||
| 64 | // mockMetrics implements metrics recording for testing | ||
| 65 | type mockMetrics struct { | ||
| 66 | mu sync.Mutex | ||
| 67 | connections int | ||
| 68 | requests map[string]int | ||
| 69 | blockedEvents map[int32]int | ||
| 70 | } | ||
| 71 | |||
| 72 | func newMockMetrics() *mockMetrics { | ||
| 73 | return &mockMetrics{ | ||
| 74 | requests: make(map[string]int), | ||
| 75 | blockedEvents: make(map[int32]int), | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | func (m *mockMetrics) IncrementConnections() { | ||
| 80 | m.mu.Lock() | ||
| 81 | defer m.mu.Unlock() | ||
| 82 | m.connections++ | ||
| 83 | } | ||
| 84 | |||
| 85 | func (m *mockMetrics) DecrementConnections() { | ||
| 86 | m.mu.Lock() | ||
| 87 | defer m.mu.Unlock() | ||
| 88 | m.connections-- | ||
| 89 | } | ||
| 90 | |||
| 91 | func (m *mockMetrics) IncrementSubscriptions() {} | ||
| 92 | func (m *mockMetrics) DecrementSubscriptions() {} | ||
| 93 | func (m *mockMetrics) SetActiveSubscriptions(count int) {} | ||
| 94 | func (m *mockMetrics) RecordRequest(method, status string, duration float64) { | ||
| 95 | m.mu.Lock() | ||
| 96 | defer m.mu.Unlock() | ||
| 97 | key := fmt.Sprintf("%s:%s", method, status) | ||
| 98 | m.requests[key]++ | ||
| 99 | } | ||
| 100 | func (m *mockMetrics) RecordBlockedEvent(kind int32) { | ||
| 101 | m.mu.Lock() | ||
| 102 | defer m.mu.Unlock() | ||
| 103 | m.blockedEvents[kind]++ | ||
| 104 | } | ||
| 105 | |||
| 106 | func (m *mockMetrics) getRequestCount(method, status string) int { | ||
| 107 | m.mu.Lock() | ||
| 108 | defer m.mu.Unlock() | ||
| 109 | return m.requests[fmt.Sprintf("%s:%s", method, status)] | ||
| 110 | } | ||
| 111 | |||
| 112 | // testServer sets up a test WebSocket server with the handler | ||
| 113 | type testServer struct { | ||
| 114 | server *httptest.Server | ||
| 115 | handler *Handler | ||
| 116 | store *mockAuthStore | ||
| 117 | metrics *mockMetrics | ||
| 118 | limiter *ratelimit.Limiter | ||
| 119 | } | ||
| 120 | |||
| 121 | func newTestServer(authConfig *AuthConfig, enableRateLimit bool) *testServer { | ||
| 122 | store := newMockAuthStore() | ||
| 123 | metrics := newMockMetrics() | ||
| 124 | subs := subscription.NewManager() | ||
| 125 | |||
| 126 | handler := NewHandler(store, subs) | ||
| 127 | handler.SetMetrics(metrics) | ||
| 128 | |||
| 129 | if authConfig != nil { | ||
| 130 | handler.SetAuth(store) | ||
| 131 | handler.SetAuthConfig(authConfig) | ||
| 132 | } | ||
| 133 | |||
| 134 | var limiter *ratelimit.Limiter | ||
| 135 | if enableRateLimit { | ||
| 136 | limiter = ratelimit.New(&ratelimit.Config{ | ||
| 137 | RequestsPerSecond: 2, // Low limit for easy testing | ||
| 138 | BurstSize: 2, | ||
| 139 | }) | ||
| 140 | handler.SetRateLimiter(limiter) | ||
| 141 | } | ||
| 142 | |||
| 143 | server := httptest.NewServer(handler) | ||
| 144 | |||
| 145 | return &testServer{ | ||
| 146 | server: server, | ||
| 147 | handler: handler, | ||
| 148 | store: store, | ||
| 149 | metrics: metrics, | ||
| 150 | limiter: limiter, | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | func (ts *testServer) Close() { | ||
| 155 | ts.server.Close() | ||
| 156 | } | ||
| 157 | |||
| 158 | func (ts *testServer) wsURL() string { | ||
| 159 | return "ws" + strings.TrimPrefix(ts.server.URL, "http") | ||
| 160 | } | ||
| 161 | |||
| 162 | // connectWS creates a WebSocket connection to the test server | ||
| 163 | func (ts *testServer) connectWS(t *testing.T) *ws.Conn { | ||
| 164 | conn, err := ws.Dial(context.Background(), ts.wsURL()) | ||
| 165 | if err != nil { | ||
| 166 | t.Fatalf("Failed to connect: %v", err) | ||
| 167 | } | ||
| 168 | return conn | ||
| 169 | } | ||
| 170 | |||
| 171 | // sendMessage sends a JSON message over WebSocket | ||
| 172 | func sendMessage(t *testing.T, conn *ws.Conn, msg interface{}) { | ||
| 173 | data, err := json.Marshal(msg) | ||
| 174 | if err != nil { | ||
| 175 | t.Fatalf("Failed to marshal message: %v", err) | ||
| 176 | } | ||
| 177 | if err := conn.Write(context.Background(), ws.MessageText, data); err != nil { | ||
| 178 | t.Fatalf("Failed to send message: %v", err) | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 182 | // sendEvent sends an EVENT message and returns the response | ||
| 183 | func sendEvent(t *testing.T, conn *ws.Conn, event *nostr.Event) []interface{} { | ||
| 184 | msg := []interface{}{"EVENT", event} | ||
| 185 | sendMessage(t, conn, msg) | ||
| 186 | return readMessage(t, conn) | ||
| 187 | } | ||
| 188 | |||
| 189 | // sendAuth sends an AUTH message | ||
| 190 | func sendAuth(t *testing.T, conn *ws.Conn, authEvent *nostr.Event) { | ||
| 191 | msg := []interface{}{"AUTH", authEvent} | ||
| 192 | sendMessage(t, conn, msg) | ||
| 193 | } | ||
| 194 | |||
| 195 | // readMessage reads a message from the WebSocket | ||
| 196 | func readMessage(t *testing.T, conn *ws.Conn) []interface{} { | ||
| 197 | _, data, err := conn.Read(context.Background()) | ||
| 198 | if err != nil { | ||
| 199 | t.Fatalf("Failed to read message: %v", err) | ||
| 200 | } | ||
| 201 | var msg []interface{} | ||
| 202 | if err := json.Unmarshal(data, &msg); err != nil { | ||
| 203 | t.Fatalf("Failed to unmarshal message: %v", err) | ||
| 204 | } | ||
| 205 | return msg | ||
| 206 | } | ||
| 207 | |||
| 208 | // readMessageWithTimeout reads a message with timeout | ||
| 209 | func readMessageWithTimeout(conn *ws.Conn, timeout time.Duration) ([]interface{}, error) { | ||
| 210 | ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
| 211 | defer cancel() | ||
| 212 | |||
| 213 | _, data, err := conn.Read(ctx) | ||
| 214 | if err != nil { | ||
| 215 | return nil, err | ||
| 216 | } | ||
| 217 | |||
| 218 | var msg []interface{} | ||
| 219 | if err := json.Unmarshal(data, &msg); err != nil { | ||
| 220 | return nil, err | ||
| 221 | } | ||
| 222 | return msg, nil | ||
| 223 | } | ||
| 224 | |||
| 225 | // createTestEvent creates a signed test event | ||
| 226 | func createTestEvent(sk nostr.SecretKey, content string) *nostr.Event { | ||
| 227 | event := &nostr.Event{ | ||
| 228 | PubKey: nostr.GetPublicKey(sk), | ||
| 229 | CreatedAt: nostr.Now(), | ||
| 230 | Kind: nostr.KindTextNote, | ||
| 231 | Tags: nostr.Tags{}, | ||
| 232 | Content: content, | ||
| 233 | } | ||
| 234 | event.Sign(sk) | ||
| 235 | return event | ||
| 236 | } | ||
| 237 | |||
| 238 | // TestAuthRequired verifies that AUTH is required when configured | ||
| 239 | func TestAuthRequired(t *testing.T) { | ||
| 240 | sk := nostr.Generate() | ||
| 241 | pubkey := nostr.GetPublicKey(sk) | ||
| 242 | |||
| 243 | authConfig := &AuthConfig{ | ||
| 244 | WriteEnabled: true, | ||
| 245 | WriteAllowedPubkeys: []string{fmt.Sprintf("%x", pubkey[:])}, | ||
| 246 | } | ||
| 247 | |||
| 248 | ts := newTestServer(authConfig, false) | ||
| 249 | defer ts.Close() | ||
| 250 | |||
| 251 | conn := ts.connectWS(t) | ||
| 252 | defer conn.Close(ws.StatusNormalClosure, "test done") | ||
| 253 | |||
| 254 | // Try to publish without auth | ||
| 255 | event := createTestEvent(sk, "test without auth") | ||
| 256 | |||
| 257 | // Send EVENT | ||
| 258 | sendMessage(t, conn, []interface{}{"EVENT", event}) | ||
| 259 | |||
| 260 | // Should receive AUTH challenge | ||
| 261 | msg1 := readMessage(t, conn) | ||
| 262 | if len(msg1) < 2 || msg1[0] != "AUTH" { | ||
| 263 | t.Fatalf("Expected AUTH challenge, got: %v", msg1) | ||
| 264 | } | ||
| 265 | challenge := msg1[1].(string) | ||
| 266 | t.Logf("Received AUTH challenge: %s", challenge) | ||
| 267 | |||
| 268 | // Should also receive OK false | ||
| 269 | msg2 := readMessage(t, conn) | ||
| 270 | if len(msg2) < 4 || msg2[0] != "OK" { | ||
| 271 | t.Fatalf("Expected OK message, got: %v", msg2) | ||
| 272 | } | ||
| 273 | if msg2[2].(bool) != false { | ||
| 274 | t.Errorf("Expected OK false, got true") | ||
| 275 | } | ||
| 276 | if !strings.Contains(msg2[3].(string), "auth-required") { | ||
| 277 | t.Errorf("Expected 'auth-required' message, got: %s", msg2[3]) | ||
| 278 | } | ||
| 279 | t.Logf("Received OK false: %v", msg2[3]) | ||
| 280 | |||
| 281 | // Now authenticate | ||
| 282 | authEvent := &nostr.Event{ | ||
| 283 | PubKey: pubkey, | ||
| 284 | CreatedAt: nostr.Now(), | ||
| 285 | Kind: 22242, | ||
| 286 | Tags: nostr.Tags{ | ||
| 287 | {"relay", ts.server.URL}, | ||
| 288 | {"challenge", challenge}, | ||
| 289 | }, | ||
| 290 | Content: "", | ||
| 291 | } | ||
| 292 | authEvent.Sign(sk) | ||
| 293 | sendAuth(t, conn, authEvent) | ||
| 294 | |||
| 295 | // Retry the EVENT | ||
| 296 | event2 := createTestEvent(sk, "test with auth") | ||
| 297 | msg3 := sendEvent(t, conn, event2) | ||
| 298 | |||
| 299 | // Should now succeed | ||
| 300 | if len(msg3) < 4 || msg3[0] != "OK" { | ||
| 301 | t.Fatalf("Expected OK message, got: %v", msg3) | ||
| 302 | } | ||
| 303 | if msg3[2].(bool) != true { | ||
| 304 | t.Errorf("Expected OK true after auth, got false: %v", msg3[3]) | ||
| 305 | } | ||
| 306 | t.Logf("Publish succeeded after auth") | ||
| 307 | } | ||
| 308 | |||
| 309 | // TestAuthNotInAllowlist verifies that pubkeys not in allowlist are rejected | ||
| 310 | func TestAuthNotInAllowlist(t *testing.T) { | ||
| 311 | allowedSk := nostr.Generate() | ||
| 312 | allowedPubkey := nostr.GetPublicKey(allowedSk) | ||
| 313 | |||
| 314 | unauthorizedSk := nostr.Generate() | ||
| 315 | |||
| 316 | authConfig := &AuthConfig{ | ||
| 317 | WriteEnabled: true, | ||
| 318 | WriteAllowedPubkeys: []string{fmt.Sprintf("%x", allowedPubkey[:])}, | ||
| 319 | } | ||
| 320 | |||
| 321 | ts := newTestServer(authConfig, false) | ||
| 322 | defer ts.Close() | ||
| 323 | |||
| 324 | conn := ts.connectWS(t) | ||
| 325 | defer conn.Close(ws.StatusNormalClosure, "test done") | ||
| 326 | |||
| 327 | event := createTestEvent(unauthorizedSk, "unauthorized test") | ||
| 328 | |||
| 329 | // Send EVENT | ||
| 330 | sendMessage(t, conn, []interface{}{"EVENT", event}) | ||
| 331 | |||
| 332 | // Receive AUTH challenge | ||
| 333 | msg1 := readMessage(t, conn) | ||
| 334 | if len(msg1) < 2 || msg1[0] != "AUTH" { | ||
| 335 | t.Fatalf("Expected AUTH challenge, got: %v", msg1) | ||
| 336 | } | ||
| 337 | challenge := msg1[1].(string) | ||
| 338 | |||
| 339 | // Receive OK false (auth required) | ||
| 340 | msg2 := readMessage(t, conn) | ||
| 341 | if msg2[0] != "OK" || msg2[2].(bool) != false { | ||
| 342 | t.Fatalf("Expected OK false, got: %v", msg2) | ||
| 343 | } | ||
| 344 | |||
| 345 | // Authenticate with unauthorized key | ||
| 346 | authEvent := &nostr.Event{ | ||
| 347 | PubKey: nostr.GetPublicKey(unauthorizedSk), | ||
| 348 | CreatedAt: nostr.Now(), | ||
| 349 | Kind: 22242, | ||
| 350 | Tags: nostr.Tags{ | ||
| 351 | {"relay", ts.server.URL}, | ||
| 352 | {"challenge", challenge}, | ||
| 353 | }, | ||
| 354 | Content: "", | ||
| 355 | } | ||
| 356 | authEvent.Sign(unauthorizedSk) | ||
| 357 | sendAuth(t, conn, authEvent) | ||
| 358 | |||
| 359 | // Retry EVENT with unauthorized key | ||
| 360 | event2 := createTestEvent(unauthorizedSk, "retry unauthorized") | ||
| 361 | msg3 := sendEvent(t, conn, event2) | ||
| 362 | |||
| 363 | // Should be rejected - not in allowlist | ||
| 364 | if len(msg3) < 3 || msg3[0] != "OK" { | ||
| 365 | t.Fatalf("Expected OK message, got: %v", msg3) | ||
| 366 | } | ||
| 367 | if msg3[2].(bool) != false { | ||
| 368 | t.Errorf("Expected OK false for unauthorized pubkey, got false: %v", msg3[3]) | ||
| 369 | } | ||
| 370 | t.Logf("Unauthorized pubkey correctly rejected: %v", msg3[3]) | ||
| 371 | } | ||
| 372 | |||
| 373 | // TestRateLimitByIP verifies that rate limiting works by IP | ||
| 374 | func TestRateLimitByIP(t *testing.T) { | ||
| 375 | ts := newTestServer(nil, true) // No auth, but rate limiting enabled | ||
| 376 | defer ts.Close() | ||
| 377 | |||
| 378 | conn := ts.connectWS(t) | ||
| 379 | defer conn.Close(ws.StatusNormalClosure, "test done") | ||
| 380 | |||
| 381 | sk := nostr.Generate() | ||
| 382 | |||
| 383 | // Rate limit is 2 req/sec with burst 2 | ||
| 384 | // So 3rd request should be blocked | ||
| 385 | |||
| 386 | successCount := 0 | ||
| 387 | rateLimitCount := 0 | ||
| 388 | |||
| 389 | for i := 0; i < 5; i++ { | ||
| 390 | event := createTestEvent(sk, fmt.Sprintf("test event %d", i)) | ||
| 391 | msg := sendEvent(t, conn, event) | ||
| 392 | |||
| 393 | if len(msg) < 3 || msg[0] != "OK" { | ||
| 394 | t.Fatalf("Expected OK message, got: %v", msg) | ||
| 395 | } | ||
| 396 | |||
| 397 | if msg[2].(bool) { | ||
| 398 | successCount++ | ||
| 399 | t.Logf("Event %d: accepted", i) | ||
| 400 | } else { | ||
| 401 | rateLimitCount++ | ||
| 402 | msgStr := "" | ||
| 403 | if len(msg) > 3 { | ||
| 404 | msgStr = msg[3].(string) | ||
| 405 | } | ||
| 406 | if !strings.Contains(msgStr, "rate-limited") { | ||
| 407 | t.Errorf("Expected 'rate-limited' message, got: %v", msgStr) | ||
| 408 | } | ||
| 409 | t.Logf("Event %d: rate limited - %v", i, msgStr) | ||
| 410 | } | ||
| 411 | |||
| 412 | time.Sleep(10 * time.Millisecond) | ||
| 413 | } | ||
| 414 | |||
| 415 | if successCount < 2 { | ||
| 416 | t.Errorf("Expected at least 2 successful requests (burst), got %d", successCount) | ||
| 417 | } | ||
| 418 | |||
| 419 | if rateLimitCount == 0 { | ||
| 420 | t.Errorf("Expected some requests to be rate limited, got 0") | ||
| 421 | } | ||
| 422 | |||
| 423 | t.Logf("Rate limiting working: %d accepted, %d rate limited", successCount, rateLimitCount) | ||
| 424 | } | ||
| 425 | |||
| 426 | // TestRateLimitByPubkey verifies that rate limiting works by authenticated pubkey | ||
| 427 | func TestRateLimitByPubkey(t *testing.T) { | ||
| 428 | sk := nostr.Generate() | ||
| 429 | pubkey := nostr.GetPublicKey(sk) | ||
| 430 | |||
| 431 | authConfig := &AuthConfig{ | ||
| 432 | WriteEnabled: true, | ||
| 433 | WriteAllowedPubkeys: []string{fmt.Sprintf("%x", pubkey[:])}, | ||
| 434 | } | ||
| 435 | |||
| 436 | ts := newTestServer(authConfig, true) // Auth + rate limiting | ||
| 437 | defer ts.Close() | ||
| 438 | |||
| 439 | conn := ts.connectWS(t) | ||
| 440 | defer conn.Close(ws.StatusNormalClosure, "test done") | ||
| 441 | |||
| 442 | // Authenticate first | ||
| 443 | event := createTestEvent(sk, "trigger auth") | ||
| 444 | sendMessage(t, conn, []interface{}{"EVENT", event}) | ||
| 445 | |||
| 446 | // Get AUTH challenge | ||
| 447 | msg1 := readMessage(t, conn) | ||
| 448 | if msg1[0] != "AUTH" { | ||
| 449 | t.Fatalf("Expected AUTH, got: %v", msg1) | ||
| 450 | } | ||
| 451 | challenge := msg1[1].(string) | ||
| 452 | |||
| 453 | // Read OK false | ||
| 454 | readMessage(t, conn) | ||
| 455 | |||
| 456 | // Send AUTH | ||
| 457 | authEvent := &nostr.Event{ | ||
| 458 | PubKey: pubkey, | ||
| 459 | CreatedAt: nostr.Now(), | ||
| 460 | Kind: 22242, | ||
| 461 | Tags: nostr.Tags{ | ||
| 462 | {"relay", ts.server.URL}, | ||
| 463 | {"challenge", challenge}, | ||
| 464 | }, | ||
| 465 | Content: "", | ||
| 466 | } | ||
| 467 | authEvent.Sign(sk) | ||
| 468 | sendAuth(t, conn, authEvent) | ||
| 469 | |||
| 470 | // Now spam events - should be rate limited by pubkey | ||
| 471 | successCount := 0 | ||
| 472 | rateLimitCount := 0 | ||
| 473 | |||
| 474 | for i := 0; i < 5; i++ { | ||
| 475 | event := createTestEvent(sk, fmt.Sprintf("spam %d", i)) | ||
| 476 | msg := sendEvent(t, conn, event) | ||
| 477 | |||
| 478 | if len(msg) < 3 || msg[0] != "OK" { | ||
| 479 | t.Fatalf("Expected OK, got: %v", msg) | ||
| 480 | } | ||
| 481 | |||
| 482 | if msg[2].(bool) { | ||
| 483 | successCount++ | ||
| 484 | t.Logf("Event %d: accepted", i) | ||
| 485 | } else { | ||
| 486 | rateLimitCount++ | ||
| 487 | msgStr := "" | ||
| 488 | if len(msg) > 3 { | ||
| 489 | msgStr = msg[3].(string) | ||
| 490 | } | ||
| 491 | t.Logf("Event %d: rate limited - %v", i, msgStr) | ||
| 492 | } | ||
| 493 | |||
| 494 | time.Sleep(10 * time.Millisecond) | ||
| 495 | } | ||
| 496 | |||
| 497 | if rateLimitCount == 0 { | ||
| 498 | t.Errorf("Expected rate limiting by pubkey, but all requests succeeded") | ||
| 499 | } | ||
| 500 | |||
| 501 | t.Logf("Rate limiting by pubkey working: %d accepted, %d rate limited", successCount, rateLimitCount) | ||
| 502 | } | ||
| 503 | |||
| 504 | // TestNoAuthWhenDisabled verifies that publishing works without auth when auth is disabled | ||
| 505 | func TestNoAuthWhenDisabled(t *testing.T) { | ||
| 506 | ts := newTestServer(nil, false) // No auth, no rate limiting | ||
| 507 | defer ts.Close() | ||
| 508 | |||
| 509 | conn := ts.connectWS(t) | ||
| 510 | defer conn.Close(ws.StatusNormalClosure, "test done") | ||
| 511 | |||
| 512 | sk := nostr.Generate() | ||
| 513 | event := createTestEvent(sk, "test without auth required") | ||
| 514 | |||
| 515 | msg := sendEvent(t, conn, event) | ||
| 516 | |||
| 517 | if len(msg) < 3 || msg[0] != "OK" { | ||
| 518 | t.Fatalf("Expected OK message, got: %v", msg) | ||
| 519 | } | ||
| 520 | |||
| 521 | if msg[2].(bool) != true { | ||
| 522 | t.Errorf("Expected OK true when auth disabled, got false: %v", msg[3]) | ||
| 523 | } | ||
| 524 | |||
| 525 | t.Logf("Publishing without auth succeeded as expected") | ||
| 526 | } | ||
