meta-ads-mcp 0.7.5__tar.gz → 0.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/PKG-INFO +1 -1
  2. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/__init__.py +1 -1
  3. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/insights.py +1 -1
  4. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/pyproject.toml +1 -1
  5. meta_ads_mcp-0.7.6/tests/test_insights_actions_and_values.py +488 -0
  6. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/.github/workflows/publish.yml +0 -0
  7. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/.github/workflows/test.yml +0 -0
  8. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/.gitignore +0 -0
  9. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/CUSTOM_META_APP.md +0 -0
  10. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/Dockerfile +0 -0
  11. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/LICENSE +0 -0
  12. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/LOCAL_INSTALLATION.md +0 -0
  13. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/META_API_NOTES.md +0 -0
  14. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/README.md +0 -0
  15. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/RELEASE.md +0 -0
  16. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/STREAMABLE_HTTP_SETUP.md +0 -0
  17. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/examples/README.md +0 -0
  18. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/examples/example_http_client.py +0 -0
  19. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/future_improvements.md +0 -0
  20. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/images/meta-ads-example.png +0 -0
  21. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_auth.sh +0 -0
  22. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/__main__.py +0 -0
  23. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/__init__.py +0 -0
  24. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/accounts.py +0 -0
  25. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/ads.py +0 -0
  26. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/ads_library.py +0 -0
  27. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/adsets.py +0 -0
  28. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/api.py +0 -0
  29. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/auth.py +0 -0
  30. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/authentication.py +0 -0
  31. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/budget_schedules.py +0 -0
  32. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/callback_server.py +0 -0
  33. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/campaigns.py +0 -0
  34. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/duplication.py +0 -0
  35. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  36. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  37. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  38. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/reports.py +0 -0
  39. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/resources.py +0 -0
  40. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/server.py +0 -0
  41. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/targeting.py +0 -0
  42. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/meta_ads_mcp/core/utils.py +0 -0
  43. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/requirements.txt +0 -0
  44. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/setup.py +0 -0
  45. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/smithery.yaml +0 -0
  46. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/README.md +0 -0
  47. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/README_REGRESSION_TESTS.md +0 -0
  48. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/__init__.py +0 -0
  49. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/conftest.py +0 -0
  50. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_account_search.py +0 -0
  51. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_budget_update.py +0 -0
  52. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_budget_update_e2e.py +0 -0
  53. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_duplication.py +0 -0
  54. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_duplication_regression.py +0 -0
  55. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_get_ad_creatives_fix.py +0 -0
  56. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_get_ad_image_regression.py +0 -0
  57. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_http_transport.py +0 -0
  58. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_integration_openai_mcp.py +0 -0
  59. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_openai.py +0 -0
  60. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_openai_mcp_deep_research.py +0 -0
  61. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_targeting.py +0 -0
  62. {meta_ads_mcp-0.7.5 → meta_ads_mcp-0.7.6}/tests/test_targeting_search_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.7.5
3
+ Version: 0.7.6
4
4
  Summary: Model Context Protocol (MCP) plugin for interacting with Meta Ads API
5
5
  Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
6
6
  Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
@@ -7,7 +7,7 @@ with the Claude LLM.
7
7
 
8
8
  from meta_ads_mcp.core.server import main
9
9
 
10
- __version__ = "0.7.5"
10
+ __version__ = "0.7.6"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -33,7 +33,7 @@ async def get_insights(access_token: str = None, object_id: str = None,
33
33
 
34
34
  endpoint = f"{object_id}/insights"
35
35
  params = {
36
- "fields": "account_id,account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,cpc,cpm,ctr,reach,frequency,actions,conversions,unique_clicks,cost_per_action_type",
36
+ "fields": "account_id,account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,cpc,cpm,ctr,reach,frequency,actions,action_values,conversions,unique_clicks,cost_per_action_type",
37
37
  "level": level
38
38
  }
39
39
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "0.7.5"
7
+ version = "0.7.6"
8
8
  description = "Model Context Protocol (MCP) plugin for interacting with Meta Ads API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unit Tests for Insights Actions and Action Values Functionality
4
+
5
+ This test suite validates the actions and action_values field implementation for the
6
+ get_insights function in meta_ads_mcp/core/insights.py.
7
+
8
+ Test cases cover:
9
+ - Actions and action_values field inclusion in API requests
10
+ - Different levels of aggregation (ad, adset, campaign, account)
11
+ - Time range handling with actions and action_values
12
+ - Error handling and validation
13
+ - Purchase data extraction from actions and action_values
14
+ """
15
+
16
+ import pytest
17
+ import json
18
+ import asyncio
19
+ from unittest.mock import AsyncMock, patch, MagicMock
20
+ from typing import Dict, Any, List
21
+
22
+ # Import the function to test
23
+ from meta_ads_mcp.core.insights import get_insights
24
+
25
+
26
+ class TestInsightsActionsAndValues:
27
+ """Test suite for actions and action_values in insights"""
28
+
29
+ @pytest.fixture
30
+ def mock_api_request(self):
31
+ """Mock for the make_api_request function"""
32
+ with patch('meta_ads_mcp.core.insights.make_api_request') as mock:
33
+ mock.return_value = {
34
+ "data": [
35
+ {
36
+ "campaign_id": "test_campaign_id",
37
+ "campaign_name": "Test Campaign",
38
+ "impressions": "1000",
39
+ "clicks": "50",
40
+ "spend": "100.00",
41
+ "actions": [
42
+ {"action_type": "purchase", "value": "5"},
43
+ {"action_type": "lead", "value": "3"},
44
+ {"action_type": "view_content", "value": "20"}
45
+ ],
46
+ "action_values": [
47
+ {"action_type": "purchase", "value": "500.00"},
48
+ {"action_type": "lead", "value": "150.00"},
49
+ {"action_type": "view_content", "value": "0.00"}
50
+ ],
51
+ "cost_per_action_type": [
52
+ {"action_type": "purchase", "value": "20.00"},
53
+ {"action_type": "lead", "value": "33.33"}
54
+ ]
55
+ }
56
+ ],
57
+ "paging": {}
58
+ }
59
+ yield mock
60
+
61
+ @pytest.fixture
62
+ def mock_auth_manager(self):
63
+ """Mock for the authentication manager"""
64
+ with patch('meta_ads_mcp.core.api.auth_manager') as mock, \
65
+ patch('meta_ads_mcp.core.api.get_current_access_token') as mock_get_token:
66
+ # Mock a valid access token
67
+ mock.get_current_access_token.return_value = "test_access_token"
68
+ mock.is_token_valid.return_value = True
69
+ mock.app_id = "test_app_id"
70
+ mock_get_token.return_value = "test_access_token"
71
+ yield mock
72
+
73
+ @pytest.fixture
74
+ def valid_campaign_id(self):
75
+ """Valid campaign ID for testing"""
76
+ return "123456789"
77
+
78
+ @pytest.fixture
79
+ def valid_account_id(self):
80
+ """Valid account ID for testing"""
81
+ return "act_701351919139047"
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_actions_and_action_values_included_in_fields(self, mock_api_request, mock_auth_manager, valid_campaign_id):
85
+ """Test that actions and action_values are included in the fields parameter"""
86
+
87
+ result = await get_insights(
88
+ object_id=valid_campaign_id,
89
+ time_range="last_30d",
90
+ level="campaign"
91
+ )
92
+
93
+ # Parse the result
94
+ result_data = json.loads(result)
95
+
96
+ # Verify the API was called with correct parameters
97
+ mock_api_request.assert_called_once()
98
+ call_args = mock_api_request.call_args
99
+
100
+ # Check that the endpoint is correct (first argument)
101
+ assert call_args[0][0] == f"{valid_campaign_id}/insights"
102
+
103
+ # Check that actions and action_values are included in fields parameter
104
+ params = call_args[0][2] # Third positional argument is params
105
+ assert 'fields' in params
106
+
107
+ fields = params['fields']
108
+ assert 'actions' in fields
109
+ assert 'action_values' in fields
110
+ assert 'cost_per_action_type' in fields
111
+
112
+ # Verify the response structure
113
+ assert 'data' in result_data
114
+ assert len(result_data['data']) > 0
115
+ assert 'actions' in result_data['data'][0]
116
+ assert 'action_values' in result_data['data'][0]
117
+
118
+ @pytest.mark.asyncio
119
+ async def test_purchase_data_extraction(self, mock_api_request, mock_auth_manager, valid_campaign_id):
120
+ """Test that purchase data can be extracted from actions and action_values"""
121
+
122
+ result = await get_insights(
123
+ object_id=valid_campaign_id,
124
+ time_range="last_30d",
125
+ level="campaign"
126
+ )
127
+
128
+ # Parse the result
129
+ result_data = json.loads(result)
130
+
131
+ # Get the first data point
132
+ data_point = result_data['data'][0]
133
+
134
+ # Extract purchase data from actions
135
+ actions = data_point.get('actions', [])
136
+ purchase_actions = [action for action in actions if action.get('action_type') == 'purchase']
137
+
138
+ # Extract purchase data from action_values
139
+ action_values = data_point.get('action_values', [])
140
+ purchase_values = [action_value for action_value in action_values if action_value.get('action_type') == 'purchase']
141
+
142
+ # Verify purchase data exists
143
+ assert len(purchase_actions) > 0, "No purchase actions found"
144
+ assert len(purchase_values) > 0, "No purchase action_values found"
145
+
146
+ # Verify purchase data values
147
+ purchase_count = purchase_actions[0].get('value')
148
+ purchase_value = purchase_values[0].get('value')
149
+
150
+ assert purchase_count == "5", f"Expected purchase count 5, got {purchase_count}"
151
+ assert purchase_value == "500.00", f"Expected purchase value 500.00, got {purchase_value}"
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_actions_at_adset_level(self, mock_api_request, mock_auth_manager, valid_campaign_id):
155
+ """Test actions and action_values at adset level"""
156
+
157
+ result = await get_insights(
158
+ object_id=valid_campaign_id,
159
+ time_range="last_30d",
160
+ level="adset"
161
+ )
162
+
163
+ # Parse the result
164
+ result_data = json.loads(result)
165
+
166
+ # Verify the API was called with correct parameters
167
+ mock_api_request.assert_called_once()
168
+ call_args = mock_api_request.call_args
169
+
170
+ # Check that the level parameter is correct
171
+ params = call_args[0][2]
172
+ assert params['level'] == 'adset'
173
+
174
+ # Verify the response structure
175
+ assert 'data' in result_data
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_actions_at_ad_level(self, mock_api_request, mock_auth_manager, valid_campaign_id):
179
+ """Test actions and action_values at ad level"""
180
+
181
+ result = await get_insights(
182
+ object_id=valid_campaign_id,
183
+ time_range="last_30d",
184
+ level="ad"
185
+ )
186
+
187
+ # Parse the result
188
+ result_data = json.loads(result)
189
+
190
+ # Verify the API was called with correct parameters
191
+ mock_api_request.assert_called_once()
192
+ call_args = mock_api_request.call_args
193
+
194
+ # Check that the level parameter is correct
195
+ params = call_args[0][2]
196
+ assert params['level'] == 'ad'
197
+
198
+ # Verify the response structure
199
+ assert 'data' in result_data
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_actions_with_custom_time_range(self, mock_api_request, mock_auth_manager, valid_campaign_id):
203
+ """Test actions and action_values with custom time range"""
204
+
205
+ custom_time_range = {"since": "2024-01-01", "until": "2024-01-31"}
206
+
207
+ result = await get_insights(
208
+ object_id=valid_campaign_id,
209
+ time_range=custom_time_range,
210
+ level="campaign"
211
+ )
212
+
213
+ # Parse the result
214
+ result_data = json.loads(result)
215
+
216
+ # Verify the API was called with correct parameters
217
+ mock_api_request.assert_called_once()
218
+ call_args = mock_api_request.call_args
219
+
220
+ # Check that time_range is properly formatted
221
+ params = call_args[0][2]
222
+ assert 'time_range' in params
223
+ assert params['time_range'] == json.dumps(custom_time_range)
224
+
225
+ # Verify the response structure
226
+ assert 'data' in result_data
227
+
228
+ @pytest.mark.asyncio
229
+ async def test_actions_with_breakdown(self, mock_api_request, mock_auth_manager, valid_campaign_id):
230
+ """Test actions and action_values with breakdown dimension"""
231
+
232
+ result = await get_insights(
233
+ object_id=valid_campaign_id,
234
+ time_range="last_30d",
235
+ level="campaign",
236
+ breakdown="age"
237
+ )
238
+
239
+ # Parse the result
240
+ result_data = json.loads(result)
241
+
242
+ # Verify the API was called with correct parameters
243
+ mock_api_request.assert_called_once()
244
+ call_args = mock_api_request.call_args
245
+
246
+ # Check that breakdown is included
247
+ params = call_args[0][2]
248
+ assert 'breakdowns' in params
249
+ assert params['breakdowns'] == 'age'
250
+
251
+ # Verify the response structure
252
+ assert 'data' in result_data
253
+
254
+ @pytest.mark.asyncio
255
+ async def test_actions_without_object_id(self, mock_api_request, mock_auth_manager):
256
+ """Test error handling when no object_id is provided"""
257
+
258
+ result = await get_insights(
259
+ time_range="last_30d",
260
+ level="campaign"
261
+ )
262
+
263
+ # Parse the result
264
+ result_data = json.loads(result)
265
+
266
+ # The error response is wrapped in a 'data' field
267
+ if 'data' in result_data:
268
+ error_data = json.loads(result_data['data'])
269
+ assert 'error' in error_data
270
+ assert 'No object ID provided' in error_data['error']
271
+ else:
272
+ assert 'error' in result_data
273
+ assert 'No object ID provided' in result_data['error']
274
+
275
+ # Verify API was not called
276
+ mock_api_request.assert_not_called()
277
+
278
+ @pytest.mark.asyncio
279
+ async def test_actions_with_invalid_time_range(self, mock_api_request, mock_auth_manager, valid_campaign_id):
280
+ """Test error handling with invalid time range"""
281
+
282
+ invalid_time_range = {"since": "2024-01-01"} # Missing "until"
283
+
284
+ result = await get_insights(
285
+ object_id=valid_campaign_id,
286
+ time_range=invalid_time_range,
287
+ level="campaign"
288
+ )
289
+
290
+ # Parse the result
291
+ result_data = json.loads(result)
292
+
293
+ # The error response is wrapped in a 'data' field
294
+ if 'data' in result_data:
295
+ error_data = json.loads(result_data['data'])
296
+ assert 'error' in error_data
297
+ assert 'since' in error_data['error'] and 'until' in error_data['error']
298
+ else:
299
+ assert 'error' in result_data
300
+ assert 'since' in result_data['error'] and 'until' in result_data['error']
301
+
302
+ # Verify API was not called
303
+ mock_api_request.assert_not_called()
304
+
305
+ @pytest.mark.asyncio
306
+ async def test_actions_api_error_handling(self, mock_api_request, mock_auth_manager, valid_campaign_id):
307
+ """Test error handling when API call fails"""
308
+
309
+ # Mock API to raise an exception
310
+ mock_api_request.side_effect = Exception("API Error")
311
+
312
+ result = await get_insights(
313
+ object_id=valid_campaign_id,
314
+ time_range="last_30d",
315
+ level="campaign"
316
+ )
317
+
318
+ # Parse the result
319
+ # The API error handling returns a dict directly, not a JSON string
320
+ if isinstance(result, dict):
321
+ result_data = result
322
+ else:
323
+ result_data = json.loads(result)
324
+
325
+ # Verify error response
326
+ assert 'error' in result_data
327
+ assert 'API Error' in result_data['error']
328
+
329
+ @pytest.mark.asyncio
330
+ async def test_actions_fields_completeness(self, mock_api_request, mock_auth_manager, valid_campaign_id):
331
+ """Test that all required fields are included in the request"""
332
+
333
+ result = await get_insights(
334
+ object_id=valid_campaign_id,
335
+ time_range="last_30d",
336
+ level="campaign"
337
+ )
338
+
339
+ # Verify the API was called with correct parameters
340
+ mock_api_request.assert_called_once()
341
+ call_args = mock_api_request.call_args
342
+
343
+ # Check that all required fields are included
344
+ params = call_args[0][2]
345
+ fields = params['fields']
346
+
347
+ # Required fields for actions and action_values
348
+ required_fields = [
349
+ 'account_id', 'account_name', 'campaign_id', 'campaign_name',
350
+ 'adset_id', 'adset_name', 'ad_id', 'ad_name',
351
+ 'impressions', 'clicks', 'spend', 'cpc', 'cpm', 'ctr',
352
+ 'reach', 'frequency', 'actions', 'action_values', 'conversions',
353
+ 'unique_clicks', 'cost_per_action_type'
354
+ ]
355
+
356
+ for field in required_fields:
357
+ assert field in fields, f"Field '{field}' not found in fields parameter"
358
+
359
+ @pytest.mark.asyncio
360
+ async def test_multiple_action_types(self, mock_api_request, mock_auth_manager, valid_campaign_id):
361
+ """Test handling of multiple action types in the response"""
362
+
363
+ result = await get_insights(
364
+ object_id=valid_campaign_id,
365
+ time_range="last_30d",
366
+ level="campaign"
367
+ )
368
+
369
+ # Parse the result
370
+ result_data = json.loads(result)
371
+
372
+ # Get the first data point
373
+ data_point = result_data['data'][0]
374
+
375
+ # Check that multiple action types are present
376
+ actions = data_point.get('actions', [])
377
+ action_types = [action.get('action_type') for action in actions]
378
+
379
+ assert 'purchase' in action_types, "Purchase action type not found"
380
+ assert 'lead' in action_types, "Lead action type not found"
381
+ assert 'view_content' in action_types, "View content action type not found"
382
+
383
+ # Check action_values has corresponding entries
384
+ action_values = data_point.get('action_values', [])
385
+ action_value_types = [action_value.get('action_type') for action_value in action_values]
386
+
387
+ assert 'purchase' in action_value_types, "Purchase action_value type not found"
388
+ assert 'lead' in action_value_types, "Lead action_value type not found"
389
+
390
+
391
+ class TestInsightsActionsAndValuesIntegration:
392
+ """Integration tests for actions and action_values functionality"""
393
+
394
+ @pytest.mark.asyncio
395
+ async def test_actions_workflow(self):
396
+ """Test complete workflow with actions and action_values"""
397
+
398
+ # This test would require actual API credentials and would be skipped in CI
399
+ # It's included for manual testing with real data
400
+ pytest.skip("Integration test requires real API credentials")
401
+
402
+ # Example workflow:
403
+ # 1. Get campaign insights with actions and action_values
404
+ # 2. Verify the data structure
405
+ # 3. Check that purchase data is present in actions and action_values
406
+ # 4. Validate the values make sense
407
+
408
+ pass
409
+
410
+
411
+ def extract_purchase_data(insights_data):
412
+ """
413
+ Helper function to extract purchase data from insights response.
414
+
415
+ Args:
416
+ insights_data: The data array from insights response
417
+
418
+ Returns:
419
+ dict: Dictionary with purchase count and value
420
+ """
421
+ if not insights_data or len(insights_data) == 0:
422
+ return {"purchase_count": 0, "purchase_value": 0.0}
423
+
424
+ data_point = insights_data[0]
425
+
426
+ # Extract purchase count from actions
427
+ actions = data_point.get('actions', [])
428
+ purchase_actions = [action for action in actions if action.get('action_type') == 'purchase']
429
+ purchase_count = int(purchase_actions[0].get('value', 0)) if purchase_actions else 0
430
+
431
+ # Extract purchase value from action_values
432
+ action_values = data_point.get('action_values', [])
433
+ purchase_values = [action_value for action_value in action_values if action_value.get('action_type') == 'purchase']
434
+ purchase_value = float(purchase_values[0].get('value', 0)) if purchase_values else 0.0
435
+
436
+ return {
437
+ "purchase_count": purchase_count,
438
+ "purchase_value": purchase_value
439
+ }
440
+
441
+
442
+ class TestPurchaseDataExtraction:
443
+ """Test suite for purchase data extraction helper function"""
444
+
445
+ def test_extract_purchase_data_with_purchases(self):
446
+ """Test extraction when purchase data is present"""
447
+ insights_data = [{
448
+ "actions": [
449
+ {"action_type": "purchase", "value": "5"},
450
+ {"action_type": "lead", "value": "3"}
451
+ ],
452
+ "action_values": [
453
+ {"action_type": "purchase", "value": "500.00"},
454
+ {"action_type": "lead", "value": "150.00"}
455
+ ]
456
+ }]
457
+
458
+ result = extract_purchase_data(insights_data)
459
+
460
+ assert result["purchase_count"] == 5
461
+ assert result["purchase_value"] == 500.0
462
+
463
+ def test_extract_purchase_data_without_purchases(self):
464
+ """Test extraction when no purchase data is present"""
465
+ insights_data = [{
466
+ "actions": [
467
+ {"action_type": "lead", "value": "3"},
468
+ {"action_type": "view_content", "value": "20"}
469
+ ],
470
+ "action_values": [
471
+ {"action_type": "lead", "value": "150.00"},
472
+ {"action_type": "view_content", "value": "0.00"}
473
+ ]
474
+ }]
475
+
476
+ result = extract_purchase_data(insights_data)
477
+
478
+ assert result["purchase_count"] == 0
479
+ assert result["purchase_value"] == 0.0
480
+
481
+ def test_extract_purchase_data_empty_data(self):
482
+ """Test extraction with empty data"""
483
+ insights_data = []
484
+
485
+ result = extract_purchase_data(insights_data)
486
+
487
+ assert result["purchase_count"] == 0
488
+ assert result["purchase_value"] == 0.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes