Skip to content

Commit 2b4c190

Browse files
authored
DX-2689 Add MFA Integration Tests (#93)
* Create test_multi_factor_authentication.py 1st pass at writing integration tests * Add docstrings and format * Remove unused imports * Change validateAuthException name Use assertAuthException to match unittest assert names * Update test.yaml * Update test.yaml * Update test.yaml * Get the 401 test working No auth header causes a 401 * Use random number generator for the verify tn request Otherwise we hit a rate limit very quickly * Newline for readability * Move seed outside of class - still seeing 429 errors * Update test/integration/test_multi_factor_authentication.py * Add test to check that the rate limit clears after 30 seconds Requested by MFA team * Remove unused error models These never get used due to the nature of the ApiException that gets raised. Overkill to initialize these by kwarg and test * Clean up spacing and remove test skips * Raise existing exception instead of a new ApiException Bump time.sleep from 30-35 * test without seeding - use systime by default * Remove manual seed for randint sets seed value as system time by default - this works better for multiple tests in parallel * Document function return types
1 parent d8109e8 commit 2b4c190

File tree

2 files changed

+181
-1
lines changed

2 files changed

+181
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ __pycache__/
66
# C extensions
77
*.so
88

9-
# MacOS Files
9+
# MacOS Files
1010
.DS_Store
1111

1212
# Distribution / packaging
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""
2+
Integration test for Bandwidth's Multi-Factor Authentication API
3+
"""
4+
5+
import os
6+
import time
7+
import unittest
8+
from random import randint
9+
10+
import bandwidth
11+
from bandwidth.api import mfa_api
12+
from bandwidth.model.code_request import CodeRequest
13+
from bandwidth.model.messaging_code_response import MessagingCodeResponse
14+
from bandwidth.model.verify_code_request import VerifyCodeRequest
15+
from bandwidth.model.verify_code_response import VerifyCodeResponse
16+
from bandwidth.model.voice_code_response import VoiceCodeResponse
17+
from bandwidth.exceptions import ApiException, UnauthorizedException, ForbiddenException
18+
19+
20+
class TestMultiFactorAuthentication(unittest.TestCase):
21+
"""Multi-Factor Authentication API integration Test
22+
"""
23+
24+
def setUp(self) -> None:
25+
configuration = bandwidth.Configuration(
26+
username=os.environ['BW_USERNAME'],
27+
password=os.environ['BW_PASSWORD']
28+
)
29+
api_client = bandwidth.ApiClient(configuration)
30+
self.api_instance = mfa_api.MFAApi(api_client)
31+
self.account_id = os.environ['BW_ACCOUNT_ID']
32+
self.messaging_code_request = CodeRequest(
33+
to=os.environ['USER_NUMBER'],
34+
_from=os.environ['BW_NUMBER'],
35+
application_id=os.environ['BW_MESSAGING_APPLICATION_ID'],
36+
scope="scope",
37+
message="Your temporary {NAME} {SCOPE} code is {CODE}",
38+
digits=6,
39+
)
40+
self.voice_code_request = CodeRequest(
41+
to=os.environ['USER_NUMBER'],
42+
_from=os.environ['BW_NUMBER'],
43+
application_id=os.environ['BW_VOICE_APPLICATION_ID'],
44+
scope="scope",
45+
message="Your temporary {NAME} {SCOPE} code is {CODE}",
46+
digits=6,
47+
)
48+
self.bad_code_request = CodeRequest(
49+
to=os.environ['USER_NUMBER'],
50+
_from=os.environ['BW_NUMBER'],
51+
application_id='not_an_application_id',
52+
scope="scope",
53+
message="Your temporary {NAME} {SCOPE} code is {CODE}",
54+
digits=6,
55+
)
56+
57+
def assertAuthException(self, context: ApiException, expectedException: ApiException, expected_status_code: int) -> None:
58+
"""Validates that an auth exception (401 or 403) is properly formatted
59+
Args:
60+
context (ApiException): Exception to validate
61+
expectedException (ApiException): Expected exception type
62+
expected_status_code (int): Expected status code
63+
"""
64+
self.assertIs(type(context.exception), expectedException)
65+
self.assertIs(type(context.exception.status), int)
66+
self.assertEqual(context.exception.status, expected_status_code)
67+
self.assertIs(type(context.exception.body), str)
68+
69+
def testSuccessfulMfaGenerateMessagingCodeRequest(self) -> None:
70+
"""Test a successful MFA messaging code request
71+
"""
72+
api_response_with_http_info = self.api_instance.generate_messaging_code(
73+
self.account_id, self.messaging_code_request,
74+
_return_http_data_only=False
75+
)
76+
self.assertEqual(api_response_with_http_info[1], 200)
77+
78+
api_response: MessagingCodeResponse = self.api_instance.generate_messaging_code(
79+
self.account_id, self.messaging_code_request)
80+
self.assertIs(type(api_response.message_id), str)
81+
82+
def testSuccessfulMfaGenerateVoiceCodeRequest(self) -> None:
83+
"""Test a successful MFA voice code request
84+
"""
85+
api_response_with_http_info = self.api_instance.generate_voice_code(
86+
self.account_id, self.voice_code_request,
87+
_return_http_data_only=False
88+
)
89+
self.assertEqual(api_response_with_http_info[1], 200)
90+
91+
api_response: VoiceCodeResponse = self.api_instance.generate_voice_code(
92+
self.account_id, self.voice_code_request)
93+
self.assertIs(type(api_response.call_id), str)
94+
95+
# Will always have to test against False codes unless we incorporate the Manteca project into MFA
96+
def testSuccessfulMfaGVerifyCodeRequest(self) -> None:
97+
"""Test a successful MFA verify code request
98+
"""
99+
verify_code_request = VerifyCodeRequest(
100+
to="+1" + str(randint(1111111111, 9999999999)),
101+
scope="2FA",
102+
expiration_time_in_minutes=3.0,
103+
code="123456",
104+
)
105+
api_response_with_http_info = self.api_instance.verify_code(
106+
self.account_id, verify_code_request,
107+
_return_http_data_only=False
108+
)
109+
self.assertEqual(api_response_with_http_info[1], 200)
110+
111+
api_response: VerifyCodeResponse = self.api_instance.verify_code(
112+
self.account_id, verify_code_request)
113+
self.assertEqual(type(api_response), VerifyCodeResponse)
114+
self.assertEqual(type(api_response.valid), bool)
115+
self.assertIs(api_response.valid, False)
116+
117+
def testBadRequest(self) -> None:
118+
"""Validates a bad (400) request
119+
"""
120+
with self.assertRaises(ApiException) as context:
121+
self.api_instance.generate_messaging_code(self.account_id, self.bad_code_request)
122+
123+
self.assertAuthException(context, ApiException, 400)
124+
125+
def testUnauthorizedRequest(self) -> None:
126+
"""Validate an unauthorized (401) request
127+
"""
128+
unauthorized_api_client = bandwidth.ApiClient()
129+
unauthorized_api_instance = mfa_api.MFAApi(unauthorized_api_client)
130+
131+
with self.assertRaises(UnauthorizedException) as context:
132+
unauthorized_api_instance.generate_messaging_code(
133+
self.account_id, self.messaging_code_request
134+
)
135+
136+
self.assertAuthException(context, UnauthorizedException, 401)
137+
138+
def testForbiddenRequest(self) -> None:
139+
"""Validate a forbidden (403) request
140+
"""
141+
configuration = bandwidth.Configuration(
142+
username=os.environ['BW_USERNAME_FORBIDDEN'],
143+
# password=os.environ['BW_PASSWORD_FORBIDDEN'],
144+
password='bad_password'
145+
)
146+
forbidden_api_client = bandwidth.ApiClient(configuration)
147+
forbidden_api_instance = mfa_api.MFAApi(forbidden_api_client)
148+
149+
with self.assertRaises(ForbiddenException) as context:
150+
forbidden_api_instance.generate_messaging_code(
151+
self.account_id, self.messaging_code_request
152+
)
153+
154+
self.assertAuthException(context, ForbiddenException, 403)
155+
156+
def testRateLimit(self) -> None:
157+
"""Validate that the API reutrns a 429 error, and that the 429 clears after 30 seconds
158+
"""
159+
verify_code_request = VerifyCodeRequest(
160+
to="+1" + str(randint(1111111111, 9999999999)),
161+
scope="2FA",
162+
expiration_time_in_minutes=3.0,
163+
code="123456",
164+
)
165+
while True:
166+
try:
167+
api_response_with_http_info = self.api_instance.verify_code(
168+
self.account_id, verify_code_request
169+
)
170+
except ApiException as e:
171+
if e.status == 429:
172+
time.sleep(35)
173+
api_response_with_http_info = self.api_instance.verify_code(
174+
self.account_id, verify_code_request,
175+
_return_http_data_only=False
176+
)
177+
self.assertEqual(api_response_with_http_info[1], 200)
178+
break
179+
else:
180+
raise e

0 commit comments

Comments
 (0)