@@ -860,3 +860,154 @@ def test(self):
860860 self .api_version_reverse ("streaming_chat" ), TestChatView .VALID_PAYLOAD , format = "json"
861861 )
862862 self .assertEqual (response .status_code , 401 )
863+
864+
865+ class TestStreamingChatViewCSRF (APIVersionTestCaseBase , WisdomServiceAPITestCaseBase ):
866+ """Test CSRF validation for the streaming_chat endpoint."""
867+
868+ api_version = "v1"
869+
870+ def setUp (self ):
871+ super ().setUp ()
872+ (org , _ ) = Organization .objects .get_or_create (id = 123 , telemetry_opt_out = False )
873+ self .user .organization = org
874+ self .user .rh_internal = True
875+ self .user .save ()
876+
877+ @override_settings (CHATBOT_DEFAULT_PROVIDER = "wisdom" )
878+ @mock .patch (
879+ "ansible_ai_connect.ai.api.model_pipelines.http.pipelines."
880+ "HttpStreamingChatBotPipeline.get_streaming_http_response" ,
881+ )
882+ def test_csrf_validation_fails_without_token (self , mock_response ):
883+ """Test that CSRF validation fails when no CSRF token is provided."""
884+
885+ mock_response .return_value = TestStreamingChatView .mocked_response (
886+ json = TestChatView .VALID_PAYLOAD
887+ )
888+
889+ with patch .object (
890+ apps .get_app_config ("ai" ),
891+ "get_model_pipeline" ,
892+ Mock (return_value = HttpStreamingChatBotPipeline (mock_pipeline_config ("http" ))),
893+ ):
894+ from rest_framework .test import APIClient
895+
896+ csrf_client = APIClient (enforce_csrf_checks = True )
897+ csrf_client .force_authenticate (user = self .user )
898+
899+ response = csrf_client .post (
900+ self .api_version_reverse ("streaming_chat" ),
901+ TestChatView .VALID_PAYLOAD ,
902+ format = "json" ,
903+ )
904+
905+ self .assertEqual (response .status_code , 403 )
906+ self .assertIn ("CSRF" , str (response .data ))
907+
908+ @override_settings (CHATBOT_DEFAULT_PROVIDER = "wisdom" )
909+ @mock .patch (
910+ "ansible_ai_connect.ai.api.model_pipelines.http.pipelines."
911+ "HttpStreamingChatBotPipeline.get_streaming_http_response" ,
912+ )
913+ def test_csrf_validation_succeeds_with_valid_token (self , mock_response ):
914+ """Test that CSRF validation succeeds when a valid CSRF token is provided."""
915+ mock_response .return_value = TestStreamingChatView .mocked_response (
916+ json = TestChatView .VALID_PAYLOAD
917+ )
918+
919+ with patch .object (
920+ apps .get_app_config ("ai" ),
921+ "get_model_pipeline" ,
922+ Mock (return_value = HttpStreamingChatBotPipeline (mock_pipeline_config ("http" ))),
923+ ):
924+ from django .middleware .csrf import _get_new_csrf_string
925+ from rest_framework .test import APIClient
926+
927+ csrf_client = APIClient (enforce_csrf_checks = True )
928+ csrf_client .force_authenticate (user = self .user )
929+
930+ token = _get_new_csrf_string ()
931+ csrf_client .cookies ["csrftoken" ] = token
932+
933+ response = csrf_client .post (
934+ self .api_version_reverse ("streaming_chat" ),
935+ TestChatView .VALID_PAYLOAD ,
936+ format = "json" ,
937+ HTTP_X_CSRFTOKEN = token ,
938+ )
939+
940+ self .assertNotEqual (response .status_code , 403 )
941+
942+ @override_settings (CHATBOT_DEFAULT_PROVIDER = "wisdom" )
943+ @mock .patch (
944+ "ansible_ai_connect.ai.api.model_pipelines.http.pipelines."
945+ "HttpStreamingChatBotPipeline.get_streaming_http_response" ,
946+ )
947+ def test_csrf_validation_fails_with_invalid_token (self , mock_response ):
948+ """Test that CSRF validation fails when an invalid CSRF token is provided."""
949+ mock_response .return_value = TestStreamingChatView .mocked_response (
950+ json = TestChatView .VALID_PAYLOAD
951+ )
952+
953+ with patch .object (
954+ apps .get_app_config ("ai" ),
955+ "get_model_pipeline" ,
956+ Mock (return_value = HttpStreamingChatBotPipeline (mock_pipeline_config ("http" ))),
957+ ):
958+ from rest_framework .test import APIClient
959+
960+ csrf_client = APIClient (enforce_csrf_checks = True )
961+ csrf_client .force_authenticate (user = self .user )
962+
963+ response = csrf_client .post (
964+ self .api_version_reverse ("streaming_chat" ),
965+ TestChatView .VALID_PAYLOAD ,
966+ format = "json" ,
967+ HTTP_X_CSRFTOKEN = "invalid_token_12345" ,
968+ )
969+
970+ self .assertEqual (response .status_code , 403 )
971+ self .assertIn ("CSRF" , str (response .data ))
972+
973+ @override_settings (CHATBOT_DEFAULT_PROVIDER = "wisdom" )
974+ @mock .patch (
975+ "ansible_ai_connect.ai.api.model_pipelines.http.pipelines."
976+ "HttpStreamingChatBotPipeline.get_streaming_http_response" ,
977+ )
978+ def test_csrf_validation_fails_with_valid_session_and_invalid_token (self , mock_response ):
979+ """Test that CSRF validation fails when a valid session present but the token is invalid.
980+
981+ This reproduces the real-world scenario described in the PR: sessionid is valid (user
982+ has a session) but the X-CSRFToken value does not match the csrftoken cookie.
983+ """
984+ mock_response .return_value = TestStreamingChatView .mocked_response (
985+ json = TestChatView .VALID_PAYLOAD
986+ )
987+
988+ with patch .object (
989+ apps .get_app_config ("ai" ),
990+ "get_model_pipeline" ,
991+ Mock (return_value = HttpStreamingChatBotPipeline (mock_pipeline_config ("http" ))),
992+ ):
993+ from rest_framework .test import APIClient
994+
995+ csrf_client = APIClient (enforce_csrf_checks = True )
996+ csrf_client .force_authenticate (user = self .user )
997+
998+ # Simulate a valid session cookie being present
999+ csrf_client .cookies ["sessionid" ] = "validsession123"
1000+
1001+ # Simulate a csrftoken cookie that does NOT match the header token
1002+ csrf_client .cookies ["csrftoken" ] = "expected_token_abc"
1003+
1004+ # Send a mismatched/invalid header token
1005+ response = csrf_client .post (
1006+ self .api_version_reverse ("streaming_chat" ),
1007+ TestChatView .VALID_PAYLOAD ,
1008+ format = "json" ,
1009+ HTTP_X_CSRFTOKEN = "invalid_token_12345" ,
1010+ )
1011+
1012+ self .assertEqual (response .status_code , 403 )
1013+ self .assertIn ("CSRF" , str (response .data ))
0 commit comments