44from services .expedite_transfer_family_kill_switch_service import (
55 ExpediteKillSwitchService ,
66 EXPECTED_SCAN_RESULTS ,
7+ STOP_WORTHY_SCAN_RESULTS ,
78 response ,
89)
910
1213def mock_transfer_client (mocker ):
1314 transfer_client = mocker .Mock ()
1415 cloudwatch_client = mocker .Mock ()
16+
1517 class ResourceNotFoundException (Exception ):
1618 pass
1719
@@ -61,6 +63,7 @@ def sns_event():
6163def extract_message (resp ):
6264 return json .loads (resp ["body" ])["message" ]
6365
66+
6467def test_response_builds_expected_http_shape ():
6568 msg = "hello world"
6669 resp = response (msg )
@@ -71,7 +74,7 @@ def test_response_builds_expected_http_shape():
7174
7275
7376def test_handle_sns_event_happy_path_infected_expedite (
74- service , sns_event , mock_transfer_client
77+ service , sns_event , mock_transfer_client
7578):
7679 mock_transfer_client .list_servers .return_value = {
7780 "Servers" : [{"ServerId" : "srv-12345" }]
@@ -87,22 +90,22 @@ def test_handle_sns_event_happy_path_infected_expedite(
8790
8891
8992def test_handle_sns_event_no_servers_disables_kill_switch (
90- service , sns_event , mock_transfer_client
93+ service , sns_event , mock_transfer_client
9194):
9295 mock_transfer_client .list_servers .return_value = {"Servers" : []}
9396
9497 resp = service .handle_sns_event (sns_event )
9598
9699 assert (
97- extract_message (resp )
98- == "Transfer family kill switch disabled – no Transfer server ID discovered"
100+ extract_message (resp )
101+ == "Transfer family kill switch disabled – no Transfer server ID discovered"
99102 )
100103 mock_transfer_client .describe_server .assert_not_called ()
101104 mock_transfer_client .stop_server .assert_not_called ()
102105
103106
104107def test_get_transfer_server_id_happy_path_reads_from_list_servers (
105- service , mock_transfer_client
108+ service , mock_transfer_client
106109):
107110 mock_transfer_client .list_servers .return_value = {
108111 "Servers" : [{"ServerId" : " srv-9999 " }]
@@ -115,7 +118,7 @@ def test_get_transfer_server_id_happy_path_reads_from_list_servers(
115118
116119
117120def test_get_transfer_server_id_returns_empty_when_no_servers (
118- service , mock_transfer_client
121+ service , mock_transfer_client
119122):
120123 mock_transfer_client .list_servers .return_value = {"Servers" : []}
121124
@@ -125,16 +128,17 @@ def test_get_transfer_server_id_returns_empty_when_no_servers(
125128
126129
127130def test_get_transfer_server_id_returns_empty_on_generic_error (
128- service , mock_transfer_client
131+ service , mock_transfer_client
129132):
130133 mock_transfer_client .list_servers .side_effect = Exception ("boom" )
131134
132135 server_id = service .get_transfer_server_id ()
133136
134137 assert server_id == ""
135138
139+
136140def test_handle_scan_message_calls_stop_server_for_infected_expedite (
137- service , sns_event , mocker
141+ service , sns_event , mocker
138142):
139143 message = json .loads (sns_event ["Records" ][0 ]["Sns" ]["Message" ])
140144 server_id = "srv-abc"
@@ -150,6 +154,7 @@ def test_handle_scan_message_calls_stop_server_for_infected_expedite(
150154 mock_stop .assert_called_once_with (server_id )
151155 assert extract_message (resp ) == "Server stopped"
152156
157+
153158def test_is_relevant_scan_result_true_for_expected_values (service ):
154159 for value in EXPECTED_SCAN_RESULTS :
155160 assert service .is_relevant_scan_result (value ) is True
@@ -160,6 +165,7 @@ def test_is_relevant_scan_result_false_for_other_values(service):
160165 assert service .is_relevant_scan_result ("" ) is False
161166 assert service .is_relevant_scan_result (None ) is False
162167
168+
163169def test_has_required_fields_true_when_bucket_and_key_present (service ):
164170 assert service .has_required_fields ("bucket" , "key" ) is True
165171
@@ -170,6 +176,7 @@ def test_has_required_fields_false_when_bucket_or_key_missing(service):
170176 assert service .has_required_fields (None , "key" ) is False
171177 assert service .has_required_fields ("bucket" , None ) is False
172178
179+
173180def test_is_quarantine_expedite_true_for_valid_quarantine_key (service ):
174181 bucket = "cloudstoragesecquarantine-xyz"
175182 key = "pre-prod-staging-bulk-store/expedite/path/file.pdf"
@@ -185,7 +192,7 @@ def test_is_quarantine_expedite_false_for_non_quarantine_bucket(service):
185192
186193
187194def test_is_quarantine_expedite_false_if_staging_bucket_not_set (
188- mock_transfer_client , monkeypatch
195+ mock_transfer_client , monkeypatch
189196):
190197 monkeypatch .delenv ("STAGING_STORE_BUCKET_NAME" , raising = False )
191198 monkeypatch .setenv ("WORKSPACE" , "pre-prod" )
@@ -213,8 +220,9 @@ def test_extract_sns_message_returns_none_for_invalid_shapes(service):
213220 assert service .extract_sns_message ({"Records" : [{}]}) is None
214221 assert service .extract_sns_message ({"Records" : [{"Sns" : {}}]}) is None
215222
223+
216224def test_stop_transfer_family_server_happy_path_stops_server (
217- service , mock_transfer_client
225+ service , mock_transfer_client
218226):
219227 mock_transfer_client .describe_server .return_value = {"Server" : {"State" : "ONLINE" }}
220228
@@ -226,7 +234,7 @@ def test_stop_transfer_family_server_happy_path_stops_server(
226234
227235
228236def test_stop_transfer_family_server_returns_not_found_if_server_missing (
229- service , mock_transfer_client
237+ service , mock_transfer_client
230238):
231239 NotFound = mock_transfer_client .exceptions .ResourceNotFoundException
232240 mock_transfer_client .describe_server .side_effect = NotFound ()
@@ -238,7 +246,7 @@ def test_stop_transfer_family_server_returns_not_found_if_server_missing(
238246
239247
240248def test_stop_transfer_family_server_handles_generic_exception (
241- service , mock_transfer_client
249+ service , mock_transfer_client
242250):
243251 mock_transfer_client .describe_server .side_effect = Exception ("boom" )
244252
@@ -247,6 +255,7 @@ def test_stop_transfer_family_server_handles_generic_exception(
247255 mock_transfer_client .stop_server .assert_not_called ()
248256 assert extract_message (resp ) == "Failed to stop server"
249257
258+
250259def test_handle_scan_message_ignores_irrelevant_scan_result (service , mocker ):
251260 message = {
252261 "scanResult" : "Clean" ,
@@ -327,11 +336,12 @@ def test_handle_scan_message_non_infected_expedite(service, mocker):
327336 resp = service .handle_scan_message (server_id = "srv-abc" , message = message )
328337
329338 assert (
330- extract_message (resp )
331- == "Non-infected result for expedite file, no kill switch action"
339+ extract_message (resp )
340+ == "Non-infected result for expedite file, no kill switch action"
332341 )
333342 mock_stop .assert_not_called ()
334343
344+
335345def test_extract_sns_message_returns_none_on_invalid_json (service ):
336346 event = {
337347 "Records" : [
@@ -347,8 +357,9 @@ def test_extract_sns_message_returns_none_on_invalid_json(service):
347357
348358 assert msg is None
349359
360+
350361def test_stop_transfer_family_server_handles_metric_failure (
351- service , mock_transfer_client , mocker
362+ service , mock_transfer_client , mocker
352363):
353364 mock_transfer_client .describe_server .return_value = {"Server" : {"State" : "ONLINE" }}
354365
@@ -362,7 +373,155 @@ def test_stop_transfer_family_server_handles_metric_failure(
362373
363374 mock_transfer_client .describe_server .assert_called_once_with (ServerId = "srv-xyz" )
364375 mock_transfer_client .stop_server .assert_called_once_with (ServerId = "srv-xyz" )
376+ assert (
377+ extract_message (resp )
378+ == "Server srv-xyz stopped, but failed to alert the team"
379+ )
380+
381+ def test_handle_sns_event_stops_for_each_stop_worthy_scan_result (
382+ service , sns_event , mock_transfer_client , monkeypatch
383+ ):
384+ mock_transfer_client .list_servers .return_value = {
385+ "Servers" : [{"ServerId" : "srv-12345" }]
386+ }
387+ mock_transfer_client .describe_server .return_value = {"Server" : {"State" : "ONLINE" }}
388+
389+ for scan_result in STOP_WORTHY_SCAN_RESULTS :
390+ message = json .loads (sns_event ["Records" ][0 ]["Sns" ]["Message" ])
391+ message ["scanResult" ] = scan_result
392+
393+ event = {
394+ "Records" : [
395+ {
396+ "Sns" : {
397+ "Message" : json .dumps (message ),
398+ }
399+ }
400+ ]
401+ }
402+
403+ resp = service .handle_sns_event (event )
404+
405+ assert extract_message (resp ) == "Server srv-12345 stopped"
406+
407+ assert mock_transfer_client .stop_server .call_count == len (STOP_WORTHY_SCAN_RESULTS )
408+
409+
410+ def test_handle_sns_event_does_not_stop_for_error_scan_result (
411+ service , sns_event , mock_transfer_client
412+ ):
413+ mock_transfer_client .list_servers .return_value = {
414+ "Servers" : [{"ServerId" : "srv-12345" }]
415+ }
416+
417+ message = json .loads (sns_event ["Records" ][0 ]["Sns" ]["Message" ])
418+ message ["scanResult" ] = "Error"
419+ event = {
420+ "Records" : [
421+ {
422+ "Sns" : {
423+ "Message" : json .dumps (message ),
424+ }
425+ }
426+ ]
427+ }
428+
429+ resp = service .handle_sns_event (event )
430+
431+ assert extract_message (resp ) == "No action taken"
432+ mock_transfer_client .describe_server .assert_not_called ()
433+ mock_transfer_client .stop_server .assert_not_called ()
434+
435+
436+ def test_handle_sns_event_does_not_stop_for_clean_or_unknown_scan_result (
437+ service , sns_event , mock_transfer_client
438+ ):
439+ mock_transfer_client .list_servers .return_value = {
440+ "Servers" : [{"ServerId" : "srv-12345" }]
441+ }
442+
443+ for scan_result in ["Clean" , "CLEAN" , "Unknown" , "NoThreatsFound" ]:
444+ message = json .loads (sns_event ["Records" ][0 ]["Sns" ]["Message" ])
445+ message ["scanResult" ] = scan_result
446+
447+ event = {
448+ "Records" : [
449+ {
450+ "Sns" : {
451+ "Message" : json .dumps (message ),
452+ }
453+ }
454+ ]
455+ }
456+
457+ resp = service .handle_sns_event (event )
458+ assert extract_message (resp ) == "No action taken"
459+
460+ mock_transfer_client .describe_server .assert_not_called ()
461+ mock_transfer_client .stop_server .assert_not_called ()
462+
463+
464+ def test_handle_sns_event_returns_no_action_when_scan_result_missing (
465+ service , sns_event , mock_transfer_client
466+ ):
467+ mock_transfer_client .list_servers .return_value = {
468+ "Servers" : [{"ServerId" : "srv-12345" }]
469+ }
470+
471+ message = json .loads (sns_event ["Records" ][0 ]["Sns" ]["Message" ])
472+ message .pop ("scanResult" , None )
473+
474+ event = {
475+ "Records" : [
476+ {
477+ "Sns" : {
478+ "Message" : json .dumps (message ),
479+ }
480+ }
481+ ]
482+ }
483+
484+ resp = service .handle_sns_event (event )
485+
486+ assert extract_message (resp ) == "No action taken"
487+ mock_transfer_client .describe_server .assert_not_called ()
488+ mock_transfer_client .stop_server .assert_not_called ()
489+
490+
491+ def test_handle_sns_event_returns_invalid_sns_message_on_bad_json (
492+ service , mock_transfer_client
493+ ):
494+ mock_transfer_client .list_servers .return_value = {
495+ "Servers" : [{"ServerId" : "srv-12345" }]
496+ }
497+
498+ event = {
499+ "Records" : [
500+ {
501+ "Sns" : {
502+ "Message" : "not-json-at-all" ,
503+ }
504+ }
505+ ]
506+ }
507+
508+ resp = service .handle_sns_event (event )
509+
510+ assert extract_message (resp ) == "Invalid SNS message; no action taken"
511+ mock_transfer_client .describe_server .assert_not_called ()
512+ mock_transfer_client .stop_server .assert_not_called ()
513+
514+
515+ def test_handle_sns_event_no_servers_message_contract (
516+ service , sns_event , mock_transfer_client
517+ ):
518+ mock_transfer_client .list_servers .return_value = {"Servers" : []}
519+
520+ resp = service .handle_sns_event (sns_event )
521+
365522 assert (
366523 extract_message (resp )
367- == "Server srv-xyz stopped, but failed to alert the team "
524+ == "Transfer family kill switch disabled – no Transfer server ID discovered "
368525 )
526+ mock_transfer_client .describe_server .assert_not_called ()
527+ mock_transfer_client .stop_server .assert_not_called ()
0 commit comments