33"""Unit tests, and local fixtures for DUO module."""
44from unittest .mock import Mock
55
6+ import pytest
7+ from tokendito .http_client import HTTP_client
68
7- def test_set_passcode (mocker ):
9+
10+ def test_get_passcode (mocker ):
811 """Check if numerical passcode can handle leading zero values."""
912 from tokendito import duo
1013
1114 mocker .patch ("tokendito.user.tty_assertion" , return_value = True )
1215 mocker .patch ("tokendito.user.input" , return_value = "0123456" )
13- assert duo .set_passcode ({"factor" : "passcode" }) == "0123456"
16+ assert duo .get_passcode ({"factor" : "passcode" }) == "0123456"
17+ assert duo .get_passcode ({"factor" : "PassCode" }) == "0123456"
18+ assert duo .get_passcode ({"factor" : "push" }) is None
19+ assert duo .get_passcode ("pytest" ) is None
1420
1521
16- def test_prepare_duo_info ():
22+ def test_prepare_info ():
1723 """Test behaviour empty return duo info."""
1824 from tokendito .config import config
19- from tokendito .duo import prepare_duo_info
25+ from tokendito .duo import prepare_info
2026
2127 selected_okta_factor = {
2228 "_embedded" : {
@@ -49,13 +55,17 @@ def test_prepare_duo_info():
4955 "sid" : "" ,
5056 "version" : "3.7" ,
5157 }
52- assert prepare_duo_info (selected_okta_factor ) == expected_duo_info
58+ assert prepare_info (selected_okta_factor ) == expected_duo_info
59+
60+ with pytest .raises (SystemExit ) as err :
61+ prepare_info ({"badresponse" : "FAIL" })
62+ assert err .value .code == 1
5363
5464
55- def test_get_duo_sid (mocker ):
65+ def test_get_sid (mocker ):
5666 """Check if got sid correct."""
5767 from tokendito .config import config
58- from tokendito .duo import get_duo_sid
68+ from tokendito .duo import get_sid
5969
6070 test_duo_info = {
6171 "okta_factor" : "okta_factor" ,
@@ -74,19 +84,290 @@ def test_get_duo_sid(mocker):
7484 duo_api_response = Mock ()
7585 duo_api_response .url = test_url
7686
77- mocker .patch ("tokendito.duo.duo_api_post " , return_value = duo_api_response )
87+ mocker .patch ("tokendito.duo.api_post " , return_value = duo_api_response )
7888
79- duo_sid_info , duo_auth_response = get_duo_sid (test_duo_info )
89+ duo_sid_info , duo_auth_response = get_sid (test_duo_info )
8090
8191 assert duo_sid_info ["sid" ] == "testval"
8292 assert duo_auth_response .url == test_url
8393
94+ mocker .patch ("tokendito.duo.api_post" , return_value = "FAIL" )
95+ with pytest .raises (SystemExit ) as err :
96+ get_sid (test_duo_info )
97+ assert err .value .code == 2
98+
8499
85100def test_get_mfa_response ():
86101 """Test if mfa verify correctly."""
87102 from tokendito .duo import get_mfa_response
88103
89104 mfa_result = Mock ()
90- mfa_result .json = Mock (return_value = {"response" : "test_response" })
91105
92- assert get_mfa_response (mfa_result ) == "test_response"
106+ # Test if response is correct
107+ assert get_mfa_response ({"response" : "test_value" }) == "test_value"
108+
109+ # Test if response is incorrect
110+ mfa_result = Mock (return_value = {"badresponse" : "FAIL" })
111+ with pytest .raises (SystemExit ) as err :
112+ get_mfa_response (mfa_result )
113+ assert err .value .code == 1
114+
115+ # Test no key available
116+ with pytest .raises (SystemExit ) as err :
117+ get_mfa_response ({"pytest" : "FAIL" })
118+ assert err .value .code == 1
119+
120+ # Test generic failure
121+ with pytest .raises (SystemExit ) as err :
122+ get_mfa_response (Mock (return_value = "FAIL" ))
123+ assert err .value .code == 1
124+
125+
126+ def test_api_post (mocker ):
127+ """Test if duo api post correctly."""
128+ from tokendito .duo import api_post
129+
130+ mock_post = mocker .patch ("requests.Session.post" )
131+ mock_resp = mocker .Mock ()
132+ mock_resp .status_code = 201
133+ mock_resp .json .return_value = {"status" : "pytest" }
134+ mock_post .return_value = mock_resp
135+
136+ response = api_post ("https://pytest/" )
137+ assert response == {"status" : "pytest" }
138+
139+
140+ def test_get_devices (mocker ):
141+ """Test that we can get a list of devices."""
142+ from tokendito .duo import get_devices
143+
144+ mock_resp = mocker .Mock ()
145+ mock_resp .status_code = 200
146+ mock_resp .content = "<html></html>"
147+
148+ # Test generic failure or empty response
149+ with pytest .raises (SystemExit ) as err :
150+ get_devices (mock_resp )
151+ assert err .value .code == 2
152+
153+ # Test no devices in list
154+ mock_resp .content = """
155+ <select name='device'>
156+ <option value='pytest_val'>pytest_text</option>
157+ </select>
158+ """
159+ assert get_devices (mock_resp ) == []
160+
161+ # Test devices in list
162+ mock_resp .content = """
163+ <select name='device'>
164+ <option value='pytest_device'>pytest_device_name</option>
165+ </select>
166+ <fieldset data-device-index='pytest_device'>
167+ <input name='factor' value='factor_type'>
168+ </fieldset>
169+ """
170+ assert get_devices (mock_resp ) == [
171+ {"device" : "pytest_device - pytest_device_name" , "factor" : "factor_type" }
172+ ]
173+
174+
175+ def test_parse_mfa_challenge ():
176+ """Test parsing the response to the challenge."""
177+ from tokendito .duo import parse_mfa_challenge
178+
179+ mfa_challenge = Mock ()
180+
181+ # Test successful challenge
182+ assert parse_mfa_challenge ({"stat" : "OK" , "response" : {"txid" : "pytest" }}) == "pytest"
183+
184+ # Test error
185+ mfa_challenge .json = Mock (return_value = {"stat" : "OK" , "response" : "error" })
186+ with pytest .raises (SystemExit ) as err :
187+ parse_mfa_challenge (mfa_challenge )
188+ assert err .value .code == 1
189+
190+ # Test no key in returned content
191+ with pytest .raises (SystemExit ) as err :
192+ parse_mfa_challenge ({"pyest" : "OK" , "badresponse" : "error" })
193+ assert err .value .code == 1
194+
195+ # Test no response in returned content
196+ mfa_challenge .json = Mock (return_value = {"stat" : "OK" , "badresponse" : "error" })
197+ with pytest .raises (SystemExit ) as err :
198+ parse_mfa_challenge (mfa_challenge )
199+ assert err .value .code == 1
200+
201+ # Test failure
202+ with pytest .raises (SystemExit ) as err :
203+ parse_mfa_challenge ({"stat" : "fail" , "response" : {"txid" : "pytest_error" }})
204+ assert err .value .code == 1
205+
206+ # Test API failure
207+ mfa_challenge .json = Mock (return_value = {"stat" : "fail" , "response" : {"txid" : "error" }})
208+ with pytest .raises (SystemExit ) as err :
209+ parse_mfa_challenge (mfa_challenge )
210+ assert err .value .code == 1
211+
212+
213+ def test_mfa_challenge (mocker ):
214+ """TODO: Test MFA challenge."""
215+ from tokendito .duo import mfa_challenge
216+
217+ with pytest .raises (SystemExit ) as err :
218+ mfa_challenge (None , None , None )
219+ assert err .value .code == 2
220+
221+ duo_info = {
222+ "okta_factor" : "okta_factor" ,
223+ "factor_id" : 1234 ,
224+ "state_token" : 12345 ,
225+ "okta_callback_url" : "http://test.okta.href" ,
226+ "tx" : "pytest_tx" ,
227+ "tile_sig" : "pytest_tile_sig" ,
228+ "parent" : "pytest_parent" ,
229+ "host" : "pytest_host" ,
230+ "sid" : "pytest_sid" ,
231+ "version" : "3.7" ,
232+ }
233+ passcode = "pytest_passcode"
234+ mfa_option = {"factor" : "pytest_factor" , "device" : "pytest_device - pytest_device_name" }
235+
236+ mocker .patch (
237+ "tokendito.duo.api_post" , return_value = {"stat" : "OK" , "response" : {"txid" : "pytest_txid" }}
238+ )
239+
240+ txid = mfa_challenge (duo_info , mfa_option , passcode )
241+ assert txid == "pytest_txid"
242+
243+
244+ def test_parse_challenge ():
245+ """Test that we can parse a challenge."""
246+ from tokendito .duo import parse_challenge
247+
248+ verify_mfa = {"status" : "SUCCESS" , "result" : "SUCCESS" , "reason" : "pytest" }
249+ assert parse_challenge (verify_mfa , None ) == ("success" , "pytest" )
250+
251+ verify_mfa = {"status" : "UNKNOWN" , "reason" : "UNKNOWN" }
252+ challenge_result = {"result" : "PYTEST" }
253+ assert parse_challenge (verify_mfa , challenge_result ) == (challenge_result , "UNKNOWN" )
254+
255+
256+ @pytest .mark .parametrize (
257+ "return_value,side_effect,expected" ,
258+ [
259+ (("success" , "pytest" ), None , "pytest" ),
260+ ((None , None ), [(None , None ), ("success" , "pytest" )], "pytest" ),
261+ (("failure" , "pytest" ), None , SystemExit ),
262+ ],
263+ )
264+ def test_mfa_verify (mocker , return_value , side_effect , expected ):
265+ """Test MFA challenge completion.
266+
267+ side_effect is utilized to return different values on different iterations.
268+ """
269+ from tokendito .duo import mfa_verify
270+
271+ mocker .patch .object (HTTP_client , "post" , return_value = None )
272+ mocker .patch ("time.sleep" , return_value = None )
273+ mocker .patch ("tokendito.duo.get_mfa_response" , return_value = "pytest" )
274+ mocker .patch (
275+ "tokendito.duo.parse_challenge" , return_value = return_value , side_effect = side_effect
276+ )
277+
278+ duo_info = {"host" : "pytest_host" , "sid" : "pytest_sid" }
279+ txid = "pytest_txid"
280+
281+ if expected == SystemExit :
282+ # Test failure as exit condition
283+ with pytest .raises (expected ) as err :
284+ mfa_verify (duo_info , txid )
285+ assert err .value .code == 2
286+ else :
287+ # Test success, failure, and iterated calls
288+ assert mfa_verify (duo_info , txid ) == expected
289+
290+
291+ def test_factor_callback (mocker ):
292+ """Test submitting factor to callback API."""
293+ from tokendito .duo import factor_callback
294+
295+ duo_info = {"host" : "pytest_host" , "sid" : "pytest_sid" , "tile_sig" : "pytest_tile_sig" }
296+ verify_mfa = {"result_url" : "/pytest_result_url" }
297+
298+ # Test successful retrieval of the cookie
299+ duo_api_response = {
300+ "stat" : "OK" ,
301+ "response" : {"txid" : "pytest_txid" , "cookie" : "pytest_cookie" },
302+ }
303+ mocker .patch ("tokendito.duo.api_post" , return_value = duo_api_response )
304+ sig_response = factor_callback (duo_info , verify_mfa )
305+ assert sig_response == "pytest_cookie:pytest_tile_sig"
306+
307+ # Test bad data passed in
308+ duo_api_response = "FAIL"
309+ mocker .patch ("tokendito.duo.api_post" , return_value = duo_api_response )
310+ with pytest .raises (SystemExit ) as err :
311+ factor_callback (duo_info , verify_mfa )
312+ assert err .value .code == 2
313+
314+ # Test bad data passed in
315+ duo_api_response = {"stat" : "FAIL" , "response" : {"cookie" : "pytest_cookie" }}
316+ duo_info = {"host" : "pytest" , "sid" : "pytest" }
317+ mocker .patch ("tokendito.duo.api_post" , return_value = duo_api_response )
318+ with pytest .raises (SystemExit ) as err :
319+ factor_callback (duo_info , verify_mfa )
320+ assert err .value .code == 2
321+
322+
323+ def test_authenticate (mocker ):
324+ """Test end to end authentication."""
325+ from tokendito .duo import authenticate
326+
327+ mocker .patch (
328+ "tokendito.duo.get_sid" ,
329+ return_value = (
330+ {
331+ "sid" : "pytest" ,
332+ "host" : "pytest" ,
333+ "state_token" : "pytest" ,
334+ "factor_id" : "pytest" ,
335+ "okta_callback_url" : "pytest" ,
336+ },
337+ "pytest" ,
338+ ),
339+ )
340+ # We mock a lot of functions here, but we're really just testing that the data can flow,
341+ # and that it can be parsed correctly to be sent to the API endpoint.
342+ mocker .patch ("tokendito.duo.get_devices" , return_value = [{"device" : "pytest - device" }])
343+ mocker .patch ("tokendito.user.select_preferred_mfa_index" , return_value = 0 )
344+ mocker .patch ("tokendito.user.input" , return_value = "0123456" )
345+ mocker .patch ("tokendito.duo.mfa_challenge" , return_value = "txid_pytest" )
346+ mocker .patch ("tokendito.duo.mfa_verify" , return_value = {"result_url" : "/pytest_result_url" })
347+ mocker .patch ("tokendito.duo.api_post" , return_value = None )
348+ mocker .patch ("tokendito.duo.factor_callback" , return_value = "pytest_cookie:pytest_tile_sig" )
349+ selected_okta_factor = {
350+ "_embedded" : {
351+ "factor" : {
352+ "_embedded" : {
353+ "verification" : {
354+ "_links" : {
355+ "complete" : {"href" : "http://test.okta.href" },
356+ "script" : {"href" : "python-v3.7" },
357+ },
358+ "signature" : "fdsafdsa:fdsfdfds:fdsfdsfds" ,
359+ "host" : "test_host" ,
360+ }
361+ },
362+ "id" : 1234 ,
363+ }
364+ },
365+ "stateToken" : 12345 ,
366+ }
367+
368+ res = authenticate (selected_okta_factor )
369+ assert {
370+ "id" : "pytest" ,
371+ "sig_response" : "pytest_cookie:pytest_tile_sig" ,
372+ "stateToken" : "pytest" ,
373+ } == res
0 commit comments