@@ -268,9 +268,13 @@ def test_samples_tracking_in_store():
268268 for i in range (3 ):
269269 store .is_attack_wave (context )
270270
271- # Check that samples are being tracked
271+ # Check that samples are being tracked (should have 1 unique sample)
272272 samples = store .get_samples_for_ip (context .remote_address )
273- assert len (samples ) == 3
273+ assert len (samples ) == 1 # Only 1 unique sample despite 3 identical requests
274+
275+ # Verify sample structure (should contain method and url only)
276+ sample = samples [0 ]
277+ assert set (sample .keys ()) == {"method" , "url" }
274278
275279 # Clear samples
276280 store .clear_samples_for_ip (context .remote_address )
@@ -280,6 +284,158 @@ def test_samples_tracking_in_store():
280284 assert len (samples ) == 0
281285
282286
287+ def test_samples_structure_and_content ():
288+ """Test that samples contain correct structure and content"""
289+ store = AttackWaveDetectorStore ()
290+ context = test_utils .generate_context (method = "POST" , route = "/.env" )
291+
292+ with patch (
293+ "aikido_zen.vulnerabilities.attack_wave_detection.attack_wave_detector.is_web_scanner" ,
294+ return_value = True ,
295+ ):
296+ # Make enough requests to trigger attack wave
297+ for i in range (15 ):
298+ store .is_attack_wave (context )
299+
300+ # Get samples
301+ samples = store .get_samples_for_ip (context .remote_address )
302+
303+ # Should have samples (number depends on uniqueness)
304+ assert len (samples ) > 0
305+
306+ # Verify each sample has correct structure
307+ for sample in samples :
308+ assert set (sample .keys ()) == {"method" , "url" }
309+ assert sample ["method" ] == "POST"
310+ assert sample ["url" ] == context .url
311+
312+
313+ def test_samples_json_serialization ():
314+ """Test that samples can be JSON serialized correctly"""
315+ import json
316+
317+ store = AttackWaveDetectorStore ()
318+ context = test_utils .generate_context ()
319+
320+ with patch (
321+ "aikido_zen.vulnerabilities.attack_wave_detection.attack_wave_detector.is_web_scanner" ,
322+ return_value = True ,
323+ ):
324+ # Make enough requests to trigger attack wave
325+ for i in range (15 ):
326+ store .is_attack_wave (context )
327+
328+ # Get samples
329+ samples = store .get_samples_for_ip (context .remote_address )
330+
331+ # Verify samples can be JSON serialized
332+ samples_json = json .dumps (samples )
333+ assert isinstance (samples_json , str )
334+
335+ # Verify samples can be deserialized
336+ parsed_samples = json .loads (samples_json )
337+ assert len (parsed_samples ) == len (samples )
338+
339+ # Verify structure is preserved
340+ for original , parsed in zip (samples , parsed_samples ):
341+ assert original ["method" ] == parsed ["method" ]
342+ assert original ["url" ] == parsed ["url" ]
343+
344+
345+ def test_samples_with_different_contexts ():
346+ """Test samples with different contexts and IPs"""
347+ store = AttackWaveDetectorStore ()
348+
349+ # Create contexts with different IPs
350+ context1 = test_utils .generate_context (ip = "1.1.1.1" , method = "GET" )
351+ context2 = test_utils .generate_context (ip = "2.2.2.2" , method = "POST" )
352+
353+ with patch (
354+ "aikido_zen.vulnerabilities.attack_wave_detection.attack_wave_detector.is_web_scanner" ,
355+ return_value = True ,
356+ ):
357+ # Make requests for both contexts
358+ for i in range (15 ):
359+ store .is_attack_wave (context1 )
360+ store .is_attack_wave (context2 )
361+
362+ # Get samples for each IP
363+ samples1 = store .get_samples_for_ip (context1 .remote_address )
364+ samples2 = store .get_samples_for_ip (context2 .remote_address )
365+
366+ # Both should have samples
367+ assert len (samples1 ) > 0
368+ assert len (samples2 ) > 0
369+
370+ # Verify structure for both
371+ for sample in samples1 :
372+ assert set (sample .keys ()) == {"method" , "url" }
373+ assert sample ["method" ] == "GET"
374+
375+ for sample in samples2 :
376+ assert set (sample .keys ()) == {"method" , "url" }
377+ assert sample ["method" ] == "POST"
378+
379+ # Clear samples for both IPs
380+ store .clear_samples_for_ip (context1 .remote_address )
381+ store .clear_samples_for_ip (context2 .remote_address )
382+
383+ # Verify both are cleared
384+ assert len (store .get_samples_for_ip (context1 .remote_address )) == 0
385+ assert len (store .get_samples_for_ip (context2 .remote_address )) == 0
386+
387+
388+ def test_samples_limit_enforcement ():
389+ """Test that sample limits are enforced"""
390+ store = AttackWaveDetectorStore ()
391+
392+ # Create a helper function to create contexts with different URLs
393+ def create_context_with_url (ip , url , method = "GET" ):
394+ from aikido_zen .context import Context
395+ from aikido_zen .helpers .headers import Headers
396+
397+ headers = Headers ()
398+ return Context (
399+ context_obj = {
400+ "remote_address" : ip ,
401+ "method" : method ,
402+ "url" : url ,
403+ "query" : {},
404+ "headers" : headers ,
405+ "body" : None ,
406+ "cookies" : {},
407+ "source" : "test" ,
408+ "route" : "/test" ,
409+ "user" : None ,
410+ "executed_middleware" : False ,
411+ "parsed_userinput" : {},
412+ }
413+ )
414+
415+ with patch (
416+ "aikido_zen.vulnerabilities.attack_wave_detection.attack_wave_detector.is_web_scanner" ,
417+ return_value = True ,
418+ ):
419+ # Create many unique contexts with different IPs to avoid cooldown
420+ for i in range (20 ):
421+ context = create_context_with_url (
422+ f"1.1.1.{ i } " , f"http://localhost/{ i } " , f"METHOD{ i % 5 } "
423+ )
424+
425+ # Make enough requests to trigger attack wave
426+ for j in range (15 ):
427+ store .is_attack_wave (context )
428+
429+ # Check a few IPs to verify sample structure
430+ for i in range (5 ):
431+ samples = store .get_samples_for_ip (f"1.1.1.{ i } " )
432+ assert len (samples ) > 0
433+
434+ # Verify structure
435+ for sample in samples :
436+ assert set (sample .keys ()) == {"method" , "url" }
437+
438+
283439@patch ("aikido_zen.storage.attack_wave_detector_store.AttackWaveDetector" )
284440def test_mock_detector_integration (mock_detector_class ):
285441 """Test integration with mocked AttackWaveDetector"""
0 commit comments