File size: 11,144 Bytes
92bfe31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
"""
backend/tests/test_rate_limiter.py
Tests for rate limiting middleware.

Tests cover:
  - Normal requests pass through
  - Rate limits trigger 429 when exceeded
  - Admin users bypass standard limits (10x multiplier)
  - Teacher users get 3x multiplier
  - Student users get standard limits
  - Deprecated enforce_rate_limit function does nothing

Run with:  pytest backend/tests/test_rate_limiter.py -v
"""

import os
import sys
from unittest.mock import MagicMock

import pytest
from fastapi import FastAPI, Request

# Add backend directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))


class TestRateLimiterKeyFunctions:
    """Test the key functions used for rate limiting."""

    def test_get_user_identifier_with_authenticated_user(self):
        """Test that UID is extracted from request.state.user."""
        from middleware.rate_limiter import _get_user_identifier

        # Create mock request with authenticated user
        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.uid = "test-uid-123"
        mock_user.role = "student"
        mock_request.state.user = mock_user
        mock_request.client.host = "127.0.0.1"

        result = _get_user_identifier(mock_request)

        assert result == "uid:test-uid-123"

    def test_get_user_identifier_without_auth(self):
        """Test fallback to IP when no authenticated user."""
        from middleware.rate_limiter import _get_user_identifier

        mock_request = MagicMock(spec=Request)
        mock_request.state.user = None
        mock_request.client.host = "192.168.1.1"

        result = _get_user_identifier(mock_request)

        assert result == "ip:192.168.1.1"

    def test_get_user_identifier_no_client(self):
        """Test fallback when no client available."""
        from middleware.rate_limiter import _get_user_identifier

        mock_request = MagicMock(spec=Request)
        mock_request.state.user = None
        mock_request.client = None

        result = _get_user_identifier(mock_request)

        assert result == "ip:unknown"

    def test_get_user_role(self):
        """Test role extraction from request.state.user."""
        from middleware.rate_limiter import _get_user_role

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "teacher"
        mock_request.state.user = mock_user

        result = _get_user_role(mock_request)

        assert result == "teacher"

    def test_get_user_role_no_user(self):
        """Test default role when no user."""
        from middleware.rate_limiter import _get_user_role

        mock_request = MagicMock(spec=Request)
        mock_request.state.user = None

        result = _get_user_role(mock_request)

        assert result == "student"

    def test_role_multiplier_admin(self):
        """Test admin gets 10x multiplier."""
        from middleware.rate_limiter import ROLE_MULTIPLIERS

        assert ROLE_MULTIPLIERS["admin"] == 10

    def test_role_multiplier_teacher(self):
        """Test teacher gets 3x multiplier."""
        from middleware.rate_limiter import ROLE_MULTIPLIERS

        assert ROLE_MULTIPLIERS["teacher"] == 3

    def test_role_multiplier_student(self):
        """Test student gets 1x multiplier."""
        from middleware.rate_limiter import ROLE_MULTIPLIERS

        assert ROLE_MULTIPLIERS["student"] == 1


class TestRateLimiterClass:
    """Test the MathPulseLimiter class."""

    def test_limiter_initialized(self):
        """Test limiter is initialized with default limits."""
        from middleware.rate_limiter import rate_limiter

        assert rate_limiter is not None
        assert rate_limiter.limiter is not None

    def test_ai_limit_student(self):
        """Test AI limit for student is base rate (20/min)."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "student"
        mock_request.state.user = mock_user

        result = rate_limiter.ai_limit(mock_request)

        assert result == "20/minute"

    def test_ai_limit_teacher(self):
        """Test AI limit for teacher is 3x (60/min)."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "teacher"
        mock_request.state.user = mock_user

        result = rate_limiter.ai_limit(mock_request)

        assert result == "60/minute"

    def test_ai_limit_admin(self):
        """Test AI limit for admin is 10x (200/min)."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "admin"
        mock_request.state.user = mock_user

        result = rate_limiter.ai_limit(mock_request)

        assert result == "200/minute"

    def test_quiz_generate_limit(self):
        """Test quiz generation limit."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "student"
        mock_request.state.user = mock_user

        result = rate_limiter.quiz_generate_limit(mock_request)

        assert result == "10/minute"

    def test_quiz_submit_limit(self):
        """Test quiz submit limit."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "student"
        mock_request.state.user = mock_user

        result = rate_limiter.quiz_submit_limit(mock_request)

        assert result == "30/minute"

    def test_auth_limit(self):
        """Test auth limit."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "student"
        mock_request.state.user = mock_user

        result = rate_limiter.auth_limit(mock_request)

        assert result == "5/minute"

    def test_leaderboard_limit(self):
        """Test leaderboard limit."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "student"
        mock_request.state.user = mock_user

        result = rate_limiter.leaderboard_limit(mock_request)

        assert result == "60/minute"

    def test_default_limit(self):
        """Test default limit."""
        from middleware.rate_limiter import rate_limiter

        mock_request = MagicMock(spec=Request)
        mock_user = MagicMock()
        mock_user.role = "student"
        mock_request.state.user = mock_user

        result = rate_limiter.default_limit(mock_request)

        assert result == "100/minute"


class TestRateLimitExceededHandler:
    """Test the rate limit exceeded handler."""

    def test_handler_returns_429_status(self):
        """Test that handler returns 429 status code."""
        from slowapi.errors import RateLimitExceeded
        from middleware.rate_limiter import _rate_limit_exceeded_handler

        mock_request = MagicMock(spec=Request)
        mock_exc = MagicMock(spec=RateLimitExceeded)
        mock_exc.retry_after = 60

        response = _rate_limit_exceeded_handler(mock_request, mock_exc)

        assert response.status_code == 429

    def test_handler_returns_json_body(self):
        """Test that handler returns proper JSON body."""
        from slowapi.errors import RateLimitExceeded
        from middleware.rate_limiter import _rate_limit_exceeded_handler

        mock_request = MagicMock(spec=Request)
        mock_exc = MagicMock(spec=RateLimitExceeded)
        mock_exc.retry_after = 30

        response = _rate_limit_exceeded_handler(mock_request, mock_exc)

        import json
        body = json.loads(response.body)

        assert body["error"] == "rate_limit_exceeded"
        assert body["message"] == "Too many requests. Please try again later."
        assert body["retry_after"] == 30

    def test_handler_includes_retry_after_header(self):
        """Test that handler includes Retry-After header."""
        from slowapi.errors import RateLimitExceeded
        from middleware.rate_limiter import _rate_limit_exceeded_handler

        mock_request = MagicMock(spec=Request)
        mock_exc = MagicMock(spec=RateLimitExceeded)
        mock_exc.retry_after = 45

        response = _rate_limit_exceeded_handler(mock_request, mock_exc)

        assert response.headers["Retry-After"] == "45"
        assert response.headers["Content-Type"] == "application/json"


class TestDeprecateEnforceRateLimit:
    """Test that old enforce_rate_limit function is deprecated."""

    def test_enforce_rate_limit_is_noop(self):
        """Test that enforce_rate_limit does nothing."""
        # Import the deprecated function
        from main import enforce_rate_limit

        mock_request = MagicMock(spec=Request)
        # Should not raise any exception - it's a no-op now
        enforce_rate_limit(mock_request, "test_bucket", 10, 60)
        # If we get here without exception, the test passes


class TestSetupRateLimiting:
    """Test setup_rate_limiting function."""

    def test_setup_adds_limiter_to_app_state(self):
        """Test that setup adds limiter to app state."""
        from middleware.rate_limiter import setup_rate_limiting
        from middleware.rate_limiter import rate_limiter

        app = FastAPI()
        setup_rate_limiting(app)

        assert hasattr(app.state, "limiter")
        assert app.state.limiter is not None

    def test_setup_adds_exception_handler(self):
        """Test that setup adds exception handler for RateLimitExceeded."""
        from middleware.rate_limiter import setup_rate_limiting

        app = FastAPI()
        setup_rate_limiting(app)

        # Exception handler registered via app.add_exception_handler


class TestEnvironmentVariables:
    """Test environment variable configuration."""

    def test_default_rates_are_configured(self):
        """Test that default rates are set from environment."""
        # The module loads env vars at import time
        # We just verify the module loaded without error
        from middleware.rate_limiter import rate_limiter
        assert rate_limiter is not None

    def test_rates_can_be_overridden(self):
        """Test that rates can be overridden via environment variables."""
        # This test verifies the env var pattern works
        # In production, these would be set before import
        original_ai = os.environ.get("RATE_LIMIT_AI_RPM")

        try:
            os.environ["RATE_LIMIT_AI_RPM"] = "30"
            # Verify the env var was set
            assert os.environ.get("RATE_LIMIT_AI_RPM") == "30"
        finally:
            if original_ai is not None:
                os.environ["RATE_LIMIT_AI_RPM"] = original_ai
            else:
                os.environ.pop("RATE_LIMIT_AI_RPM", None)


if __name__ == "__main__":
    pytest.main([__file__, "-v"])