@@ -9,11 +9,18 @@ import (
99 "path/filepath"
1010 "strings"
1111 "testing"
12+ "time"
1213
1314 "github.com/RandomCodeSpace/ctm/internal/serve/api"
1415 "github.com/RandomCodeSpace/ctm/internal/serve/auth"
1516)
1617
18+ // testLimiter returns a generously-sized limiter so tests that aren't
19+ // specifically exercising rate-limiting are never throttled.
20+ func testLimiter () * auth.Limiter {
21+ return auth .NewLimiter (1000 , time .Second )
22+ }
23+
1724// ---------- helpers --------------------------------------------------------
1825
1926func authTempHome (t * testing.T ) string {
@@ -156,7 +163,7 @@ func TestLogin_HappyPath(t *testing.T) {
156163 enc , _ := auth .Hash ("password123" )
157164 _ = auth .Save (auth.User {Username : "alice@example.com" , Password : enc })
158165 store := auth .NewStore ()
159- h := api .AuthLogin (store )
166+ h := api .AuthLogin (store , testLimiter () )
160167 rec := httptest .NewRecorder ()
161168 h (rec , authJSONReq (t , http .MethodPost , "/api/auth/login" ,
162169 map [string ]string {"username" : "alice@example.com" , "password" : "password123" }))
@@ -175,7 +182,7 @@ func TestLogin_BadPassword(t *testing.T) {
175182 enc , _ := auth .Hash ("password123" )
176183 _ = auth .Save (auth.User {Username : "alice@example.com" , Password : enc })
177184 store := auth .NewStore ()
178- h := api .AuthLogin (store )
185+ h := api .AuthLogin (store , testLimiter () )
179186 rec := httptest .NewRecorder ()
180187 h (rec , authJSONReq (t , http .MethodPost , "/api/auth/login" ,
181188 map [string ]string {"username" : "alice@example.com" , "password" : "wrong" }))
@@ -192,7 +199,7 @@ func TestLogin_UnknownUsername(t *testing.T) {
192199 enc , _ := auth .Hash ("password123" )
193200 _ = auth .Save (auth.User {Username : "alice@example.com" , Password : enc })
194201 store := auth .NewStore ()
195- h := api .AuthLogin (store )
202+ h := api .AuthLogin (store , testLimiter () )
196203 rec := httptest .NewRecorder ()
197204 h (rec , authJSONReq (t , http .MethodPost , "/api/auth/login" ,
198205 map [string ]string {"username" : "mallory@example.com" , "password" : "password123" }))
@@ -204,7 +211,7 @@ func TestLogin_UnknownUsername(t *testing.T) {
204211func TestLogin_NotRegistered (t * testing.T ) {
205212 authTempHome (t )
206213 store := auth .NewStore ()
207- h := api .AuthLogin (store )
214+ h := api .AuthLogin (store , testLimiter () )
208215 rec := httptest .NewRecorder ()
209216 h (rec , authJSONReq (t , http .MethodPost , "/api/auth/login" ,
210217 map [string ]string {"username" : "alice@example.com" , "password" : "password123" }))
@@ -216,6 +223,42 @@ func TestLogin_NotRegistered(t *testing.T) {
216223 }
217224}
218225
226+ func TestLogin_RateLimited (t * testing.T ) {
227+ authTempHome (t )
228+ enc , _ := auth .Hash ("password123" )
229+ _ = auth .Save (auth.User {Username : "alice@example.com" , Password : enc })
230+ store := auth .NewStore ()
231+
232+ // Injected clock stays fixed so all 6 attempts fall in the window.
233+ clock := func () time.Time { return time .Unix (1_700_000_000 , 0 ) }
234+ lim := auth .NewLimiterWithClock (5 , 60 * time .Second , clock )
235+ h := api .AuthLogin (store , lim )
236+
237+ // 5 bad attempts — all should reach the handler and return 401.
238+ for i := 0 ; i < 5 ; i ++ {
239+ rec := httptest .NewRecorder ()
240+ h (rec , authJSONReq (t , http .MethodPost , "/api/auth/login" ,
241+ map [string ]string {"username" : "alice@example.com" , "password" : "wrong" }))
242+ if rec .Code != http .StatusUnauthorized {
243+ t .Fatalf ("attempt %d status = %d, want 401 (%s)" , i + 1 , rec .Code , rec .Body .String ())
244+ }
245+ }
246+
247+ // 6th attempt must be rate-limited.
248+ rec := httptest .NewRecorder ()
249+ h (rec , authJSONReq (t , http .MethodPost , "/api/auth/login" ,
250+ map [string ]string {"username" : "alice@example.com" , "password" : "wrong" }))
251+ if rec .Code != http .StatusTooManyRequests {
252+ t .Fatalf ("6th attempt status = %d, want 429" , rec .Code )
253+ }
254+ if ra := rec .Result ().Header .Get ("Retry-After" ); ra == "" {
255+ t .Fatal ("Retry-After header missing on 429" )
256+ }
257+ if ! strings .Contains (rec .Body .String (), "rate_limited" ) {
258+ t .Fatalf ("body = %q, want rate_limited code" , rec .Body .String ())
259+ }
260+ }
261+
219262// ---------- logout ---------------------------------------------------------
220263
221264func TestLogout_RevokesToken (t * testing.T ) {
0 commit comments