-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathtest_forms.py
More file actions
300 lines (246 loc) · 12.1 KB
/
test_forms.py
File metadata and controls
300 lines (246 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
from unittest.mock import Mock, PropertyMock, patch
import pytest
from django.forms.widgets import HiddenInput, Select
from apps.channels.forms import ChannelForm, SlackChannelForm, WhatsappChannelForm
from apps.channels.models import ChannelPlatform
from apps.service_providers.models import MessagingProvider, MessagingProviderType
from apps.utils.factories.channels import ExperimentChannelFactory
from apps.utils.factories.service_provider_factories import MessagingProviderFactory
from apps.utils.factories.team import TeamWithUsersFactory
@pytest.mark.parametrize(
("platform", "expected_widget_cls"),
[
("whatsapp", Select),
("telegram", HiddenInput),
],
)
def test_channel_form_reveals_provider_types(experiment, platform, expected_widget_cls):
"""Test that the message provider field is being hidden when not applicable to a certain platform"""
# First create a messaging provider
message_provider = MessagingProviderFactory.create(type=MessagingProviderType("twilio"), team=experiment.team)
MessagingProviderFactory.create(type=MessagingProviderType("twilio"))
form = ChannelForm(initial={"platform": ChannelPlatform(platform)}, experiment=experiment)
widget = form.fields["messaging_provider"].widget
assert isinstance(widget, expected_widget_cls)
form_queryset = form.fields["messaging_provider"].queryset
assert form_queryset.count() == MessagingProvider.objects.filter(team=experiment.team).count()
assert form_queryset.first() == message_provider
@pytest.mark.parametrize(
("number", "is_valid"),
[
("+27812345678", True),
("0812345678", False),
("+27 81 234 5678", True),
("+27-81-234-5678", True),
("+27-81 2345678", True),
("+27_81_234_5678", False),
("0800 100 030", False),
("+32 (0)27888484", True),
],
)
@patch("apps.channels.forms.WhatsappChannelForm.messaging_provider")
def test_whatsapp_form_validates_number_format(experiment, number, is_valid):
form = WhatsappChannelForm(experiment=experiment, data={"number": number})
assert form.is_valid() == is_valid
if not is_valid:
assert form.errors["number"] == ["Enter a valid phone number (e.g. +12125552368)."]
@pytest.mark.django_db()
@pytest.mark.parametrize(
("provider_type", "number", "number_found_at_provider"),
[
(MessagingProviderType.twilio, "+12125552368", True),
(MessagingProviderType.twilio, "+12125552333", False),
# Turnio doesn't have a way to list account numbers, so assume it's always valid
(MessagingProviderType.turnio, "+12125552368", True),
(MessagingProviderType.turnio, "+12125552333", True),
],
)
@patch("apps.channels.forms.ExtraFormBase.messaging_provider", new_callable=PropertyMock)
@patch("apps.service_providers.messaging_service.TwilioService._get_account_numbers")
def test_whatsapp_form_checks_number(
_get_account_numbers, messaging_provider, provider_type, number, number_found_at_provider, experiment
):
_get_account_numbers.return_value = ["+12125552368"]
provider = MessagingProviderFactory.create(type=provider_type, config={"account_sid": "123", "auth_token": "123"})
messaging_provider.return_value = provider
form = WhatsappChannelForm(experiment=experiment, data={"number": number, "messaging_provider": provider.id})
assert form.is_valid(), f"Form errors: {form.errors}"
if not number_found_at_provider:
assert form.warning_message == (
f"{number} was not found at the provider. Please make sure it is there before proceeding"
)
@pytest.mark.django_db()
@patch("apps.channels.forms.ExtraFormBase.messaging_provider", new_callable=PropertyMock)
@patch("apps.service_providers.messaging_service.httpx.get")
def test_whatsapp_form_meta_cloud_api_resolves_phone_number_id(mock_httpx_get, messaging_provider, experiment):
"""Test that the phone number ID is fetched from Meta API and stored in extra_data"""
import httpx
mock_httpx_get.return_value = httpx.Response(
200,
json={
"data": [
{"id": "12345", "display_phone_number": "+1 (212) 555-2368"},
]
},
request=httpx.Request("GET", "https://test"),
)
provider = MessagingProviderFactory.create(
type=MessagingProviderType.meta_cloud_api,
config={"access_token": "test_token", "business_id": "biz_123"},
)
messaging_provider.return_value = provider
form = WhatsappChannelForm(
experiment=experiment, data={"number": "+12125552368", "messaging_provider": provider.id}
)
assert form.is_valid(), f"Form errors: {form.errors}"
# ChannelFormWrapper.save() passes extra_form.cleaned_data as channel.extra_data,
# so phone_number_id should be in cleaned_data directly.
assert form.cleaned_data["phone_number_id"] == "12345"
assert form.cleaned_data["number"] == "+12125552368"
@pytest.mark.django_db()
@patch("apps.channels.forms.ExtraFormBase.messaging_provider", new_callable=PropertyMock)
@patch("apps.service_providers.messaging_service.httpx.get")
def test_whatsapp_form_meta_cloud_api_rejects_unknown_number(mock_httpx_get, messaging_provider, experiment):
"""Test that form validation fails when the phone number is not found in the Meta Business Account"""
import httpx
mock_httpx_get.return_value = httpx.Response(
200,
json={"data": []},
request=httpx.Request("GET", "https://test"),
)
provider = MessagingProviderFactory.create(
type=MessagingProviderType.meta_cloud_api,
config={"access_token": "test_token", "business_id": "biz_123"},
)
messaging_provider.return_value = provider
form = WhatsappChannelForm(
experiment=experiment, data={"number": "+12125552368", "messaging_provider": provider.id}
)
assert not form.is_valid()
assert "was not found in the WhatsApp Business Account" in form.errors["number"][0]
# Slack channel keyword uniqueness tests
@pytest.mark.django_db()
def test_slack_channel_new_with_keywords_succeeds(team_with_users, experiment):
"""Test creating a new Slack channel with keywords succeeds"""
# Create messaging provider
provider = MessagingProviderFactory.create(type=MessagingProviderType.slack, team=team_with_users)
# Mock the messaging service
mock_service = Mock()
mock_service.get_channel_by_name.return_value = None # Not using specific channel
with patch.object(provider, "get_messaging_service", return_value=mock_service):
form_data = {
"channel_scope": "all",
"routing_method": "keywords",
"keywords": "health, benefits, support",
"messaging_provider": provider.id,
}
form = SlackChannelForm(experiment=experiment, data=form_data)
form.messaging_provider = provider
assert form.is_valid(), f"Form errors: {form.errors}"
cleaned_data = form.cleaned_data
assert cleaned_data["keywords"] == ["health", "benefits", "support"]
assert cleaned_data["slack_channel_id"] == "*"
assert not cleaned_data["is_default"]
@pytest.mark.django_db()
def test_slack_channel_edit_keeping_some_keywords_succeeds(team_with_users, experiment):
"""Test editing existing channel keeping some keywords succeeds"""
# Create messaging provider
provider = MessagingProviderFactory.create(type=MessagingProviderType.slack, team=team_with_users)
# Create the channel we want to edit - this simulates the Health Bot from browser
health_bot = ExperimentChannelFactory.create(
team=team_with_users,
platform=ChannelPlatform.SLACK,
messaging_provider=provider,
name="Health Bot",
extra_data={
"slack_channel_id": "*",
"keywords": ["health", "benefits", "medical", "insurance", "deductible", "copay", "coverage"],
"is_default": False,
},
)
# Mock the messaging service
mock_service = Mock()
mock_service.get_channel_by_name.return_value = None
with patch.object(provider, "get_messaging_service", return_value=mock_service):
# Simulate editing the Health Bot to reduce keywords but keep some existing ones
form_data = {
"channel_scope": "all",
"routing_method": "keywords",
"keywords": "health, benefits, nutrition", # Keep "health" and "benefits", add "nutrition" (no conflict)
"messaging_provider": provider.id,
}
# This simulates the browser scenario - editing an existing channel
form = SlackChannelForm(
experiment=experiment, data=form_data, initial=health_bot.extra_data, channel=health_bot
)
form.messaging_provider = provider
form.instance = health_bot # This should be set by the channel parameter
# This should succeed - editing a channel should allow keeping its own existing keywords
assert form.is_valid(), f"Form errors: {form.errors}"
cleaned_data = form.cleaned_data
assert cleaned_data["keywords"] == ["health", "benefits", "nutrition"]
@pytest.mark.django_db()
def test_slack_channel_duplicate_keywords_fails(team_with_users, experiment):
"""Test creating new channel with existing keywords fails"""
# Create messaging provider
provider = MessagingProviderFactory.create(
type=MessagingProviderType.slack, team=team_with_users, config={"slack_team_id": "123"}
)
# Create existing channel with keywords
ExperimentChannelFactory.create(
team=team_with_users,
platform=ChannelPlatform.SLACK,
messaging_provider=provider,
name="Existing Bot",
extra_data={"slack_channel_id": "*", "keywords": ["health", "benefits"], "is_default": False},
)
# Mock the messaging service
mock_service = Mock()
mock_service.get_channel_by_name.return_value = None
with patch.object(provider, "get_messaging_service", return_value=mock_service):
# Try to create new channel with overlapping keywords
form_data = {
"channel_scope": "all",
"routing_method": "keywords",
"keywords": "health, medical", # "health" conflicts
"messaging_provider": provider.id,
}
form = SlackChannelForm(experiment=experiment, data=form_data)
form.messaging_provider = provider
assert not form.is_valid()
assert "keywords" in form.errors
@pytest.mark.django_db()
def test_slack_channel_cross_team_keyword_conflicts(team_with_users, experiment):
"""Test that keyword conflicts are validated system-wide across teams"""
# Create messaging provider
provider = MessagingProviderFactory.create(
type=MessagingProviderType.slack, team=team_with_users, config={"slack_team_id": "123"}
)
# Create a different team that shares the same Slack workspace (same messaging provider)
other_team = TeamWithUsersFactory.create()
# Create existing channel in the OTHER team with keywords
ExperimentChannelFactory.create(
team=other_team, # Different team!
platform=ChannelPlatform.SLACK,
messaging_provider=provider, # Same messaging provider (same Slack workspace)
name="Other Team Bot",
extra_data={"slack_channel_id": "*", "keywords": ["health", "benefits"], "is_default": False},
)
# Mock the messaging service
mock_service = Mock()
mock_service.get_channel_by_name.return_value = None
with patch.object(provider, "get_messaging_service", return_value=mock_service):
# Try to create new channel in current team with conflicting keywords
form_data = {
"channel_scope": "all",
"routing_method": "keywords",
"keywords": "health, wellness", # "health" conflicts with other team's bot
"messaging_provider": provider.id,
}
form = SlackChannelForm(experiment=experiment, data=form_data)
form.messaging_provider = provider
# Should fail because keywords must be unique across ALL teams using the same Slack workspace
assert not form.is_valid(), f"Form errors: {form.errors}"
error_message = str(form.errors)
assert "Other Team Bot" not in error_message
assert "health" in error_message