11import json
22
3+ import pytest
4+ import responses
35from django .urls import reverse
6+ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped]
47from pytest_mock import MockerFixture
58from rest_framework import status
69from rest_framework .test import APIClient
710
11+ WEBHOOK_URL = "https://example.com/webhook"
812
13+
14+ @pytest .fixture
15+ def environment_webhook (
16+ admin_client : APIClient ,
17+ environment : int ,
18+ environment_api_key : str ,
19+ ) -> str :
20+ """Create an environment webhook via API and return its URL."""
21+ url = reverse (
22+ "api-v1:environments:environment-webhooks-list" ,
23+ args = [environment_api_key ],
24+ )
25+ response = admin_client .post (url , data = {"url" : WEBHOOK_URL , "enabled" : True })
26+ assert response .status_code == status .HTTP_201_CREATED
27+ return WEBHOOK_URL
28+
29+
30+ @pytest .fixture
31+ def organisation_webhook (
32+ admin_client : APIClient ,
33+ organisation : int ,
34+ ) -> str :
35+ """Create an organisation webhook via API and return its URL."""
36+ url = reverse (
37+ "api-v1:organisations:organisation-webhooks-list" ,
38+ args = [organisation ],
39+ )
40+ response = admin_client .post (url , data = {"url" : WEBHOOK_URL , "enabled" : True })
41+ assert response .status_code == status .HTTP_201_CREATED
42+ return WEBHOOK_URL
43+
44+
45+ @responses .activate
946def test_update_segment_override__webhook_payload_has_correct_previous_and_new_values (
1047 admin_client : APIClient ,
1148 environment : int ,
1249 feature : int ,
1350 segment : int ,
14- mocker : MockerFixture ,
51+ environment_webhook : str ,
1552) -> None :
1653 """
1754 Test for issue #6050: Webhook payload shows incorrect previous_state values
@@ -26,6 +63,8 @@ def test_update_segment_override__webhook_payload_has_correct_previous_and_new_v
2663 old_value = 0
2764 new_value = 1
2865
66+ responses .add (responses .POST , environment_webhook , status = 200 )
67+
2968 # First create a feature_segment
3069 feature_segment_url = reverse ("api-v1:features:feature-segment-list" )
3170 feature_segment_data = {
@@ -56,11 +95,8 @@ def test_update_segment_override__webhook_payload_has_correct_previous_and_new_v
5695 assert create_response .status_code == status .HTTP_201_CREATED
5796 segment_override_id = create_response .json ()["id" ]
5897
59- # Mock call_environment_webhooks to capture the actual payload
60- mock_call_environment_webhooks = mocker .patch (
61- "features.tasks.call_environment_webhooks"
62- )
63- mocker .patch ("features.tasks.call_organisation_webhooks" )
98+ # Clear responses from create operation
99+ responses .calls .reset ()
64100
65101 # When - update the segment override via API
66102 url = reverse ("api-v1:features:featurestates-detail" , args = [segment_override_id ])
@@ -78,12 +114,107 @@ def test_update_segment_override__webhook_payload_has_correct_previous_and_new_v
78114 # Then
79115 assert response .status_code == status .HTTP_200_OK
80116
81- # Verify webhook was called
82- mock_call_environment_webhooks .delay .assert_called_once ()
83- webhook_args = mock_call_environment_webhooks .delay .call_args .kwargs ["args" ]
84- webhook_payload = webhook_args [1 ] # (environment_id, data, event_type)
117+ # Verify webhook was called with correct payload
118+ assert len (responses .calls ) == 1
119+ webhook_payload = json .loads (responses .calls [0 ].request .body )["data" ] # type: ignore[union-attr]
85120
86121 # Verify the payload has correct values
87122 assert webhook_payload ["new_state" ]["feature_segment" ] is not None
88123 assert webhook_payload ["new_state" ]["feature_state_value" ] == new_value
89124 assert webhook_payload ["previous_state" ]["feature_state_value" ] == old_value
125+
126+
127+ @pytest .mark .parametrize (
128+ "webhook" ,
129+ [lazy_fixture ("environment_webhook" ), lazy_fixture ("organisation_webhook" )],
130+ )
131+ @responses .activate
132+ def test_update_multivariate_percentage__webhook_payload_includes_multivariate_values (
133+ admin_client : APIClient ,
134+ environment : int ,
135+ feature : int ,
136+ mv_option_50_percent : int ,
137+ mv_option_value : str ,
138+ webhook : str ,
139+ mocker : MockerFixture ,
140+ ) -> None :
141+ """
142+ Test for issue #6190: Webhook payloads do not include multivariate values.
143+
144+ When updating the percentage allocation of a multivariate option,
145+ the webhook payload should include the multivariate_feature_state_values
146+ with their percentage allocations.
147+ """
148+ # Given
149+ # Get the feature state for this environment
150+ feature_states_url = reverse ("api-v1:features:featurestates-list" )
151+ feature_states_response = admin_client .get (
152+ f"{ feature_states_url } ?environment={ environment } &feature={ feature } "
153+ )
154+ assert feature_states_response .status_code == status .HTTP_200_OK
155+ feature_state = feature_states_response .json ()["results" ][0 ]
156+ feature_state_id = feature_state ["id" ]
157+
158+ # Get current multivariate feature state values
159+ assert len (feature_state ["multivariate_feature_state_values" ]) == 1
160+ mv_fs_value = feature_state ["multivariate_feature_state_values" ][0 ]
161+ old_percentage = mv_fs_value ["percentage_allocation" ]
162+ new_percentage = 75
163+
164+ responses .add (responses .POST , webhook , status = 200 )
165+
166+ # When
167+ # update only the multivariate percentage allocation
168+ url = reverse ("api-v1:features:featurestates-detail" , args = [feature_state_id ])
169+ data = {
170+ "id" : feature_state_id ,
171+ "enabled" : feature_state ["enabled" ],
172+ "feature_state_value" : feature_state ["feature_state_value" ],
173+ "environment" : environment ,
174+ "feature" : feature ,
175+ "multivariate_feature_state_values" : [
176+ {
177+ "id" : mv_fs_value ["id" ],
178+ "multivariate_feature_option" : mv_fs_value [
179+ "multivariate_feature_option"
180+ ],
181+ "percentage_allocation" : new_percentage ,
182+ }
183+ ],
184+ }
185+ admin_client .put (url , data = json .dumps (data ), content_type = "application/json" )
186+
187+ # Then
188+ # `FLAG_UPDATED`` webhook was called
189+ # (should be the last sent event)
190+ last_call = responses .calls [- 1 ]
191+ assert not isinstance (last_call , list )
192+ webhook_payload = json .loads (last_call .request .body )
193+ assert webhook_payload ["event_type" ] == "FLAG_UPDATED"
194+
195+ # the payload includes multivariate values
196+ event_data = webhook_payload ["data" ]
197+
198+ assert "multivariate_feature_state_values" in event_data ["new_state" ]
199+ assert "multivariate_feature_state_values" in event_data ["previous_state" ]
200+
201+ assert event_data ["new_state" ]["multivariate_feature_state_values" ] == [
202+ {
203+ "id" : mocker .ANY ,
204+ "multivariate_feature_option" : {
205+ "id" : mv_option_50_percent ,
206+ "value" : mv_option_value ,
207+ },
208+ "percentage_allocation" : new_percentage ,
209+ },
210+ ]
211+ assert event_data ["previous_state" ]["multivariate_feature_state_values" ] == [
212+ {
213+ "id" : mocker .ANY ,
214+ "multivariate_feature_option" : {
215+ "id" : mv_option_50_percent ,
216+ "value" : mv_option_value ,
217+ },
218+ "percentage_allocation" : old_percentage ,
219+ },
220+ ]
0 commit comments