From bfae04723bd3155801f079ab5238018ec8a0cb09 Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Fri, 1 May 2026 14:34:04 +0200 Subject: Redesign API by introducing otp2289.OTPResponse type --- tests/test_generator.py | 278 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 192 insertions(+), 86 deletions(-) (limited to 'tests/test_generator.py') diff --git a/tests/test_generator.py b/tests/test_generator.py index 08947e5..68d3bfe 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -35,26 +35,26 @@ def test_caller_exceptions() -> None: b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5 ) with pytest.raises(otp2289.OTPGeneratorError) as exc_info: - gen.generate_otp_words('3') # ty: ignore[invalid-argument-type] + gen.generate_otp_response('3') # ty: ignore[invalid-argument-type] assert exc_info.type is otp2289.OTPGeneratorError assert exc_info.value.args[0] == 'Step value MUST be an int' with pytest.raises(otp2289.OTPGeneratorError) as exc_info: - gen.generate_otp_hexdigest(-1) + gen.generate_otp_response(-1) assert exc_info.type is otp2289.OTPGeneratorError assert exc_info.value.args[0] == 'Step value MUST be >= 0' with pytest.raises(otp2289.OTPChallengeError) as exc_info: - gen.generate_otp_hexdigest_from_challenge( + gen.generate_otp_response_from_challenge( b'md5 fbd TeSt' # ty: ignore[invalid-argument-type] ) assert exc_info.type is otp2289.OTPChallengeError assert exc_info.value.args[0] == 'Challenge must be str' with pytest.raises(otp2289.OTPChallengeError) as exc_info: - gen.generate_otp_hexdigest_from_challenge('md5 fbd TeSt') + gen.generate_otp_response_from_challenge('md5 fbd TeSt') assert exc_info.type is otp2289.OTPChallengeError assert exc_info.value.args[0] == 'Invalid challenge' - with pytest.raises(otp2289.generator.OTPChallengeError) as exc_info: - gen.generate_otp_hexdigest_from_challenge('otp-md5 fbd TeSt') - assert exc_info.type is otp2289.generator.OTPChallengeError + with pytest.raises(otp2289.OTPChallengeError) as exc_info: + gen.generate_otp_response_from_challenge('otp-md5 fbd TeSt') + assert exc_info.type is otp2289.OTPChallengeError assert exc_info.value.args[0] == 'Invalid challenge' @@ -102,9 +102,9 @@ def test_constructor_exceptions() -> None: assert exc_info.type is otp2289.OTPGeneratorError assert exc_info.value.args[0] == 'hash_algo must be an int or a str' # test the package structure as well - with pytest.raises(otp2289.generator.OTPGeneratorError) as exc_info: - otp2289.generator.OTPGenerator(b'This is a test.', 'TeSt', 'foo') - assert exc_info.type is otp2289.generator.OTPGeneratorError + with pytest.raises(otp2289.OTPGeneratorError) as exc_info: + otp2289.OTPGenerator(b'This is a test.', 'TeSt', 'foo') + assert exc_info.type is otp2289.OTPGeneratorError assert exc_info.value.args[0] == ( 'foo is not supported by this version of the hashlib module' ) @@ -133,59 +133,148 @@ def test_md5() -> None: gen = otp2289.OTPGenerator( b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5 ) - res_words = gen.generate_otp_words(0) - res_hex = gen.generate_otp_hexdigest(0) - assert isinstance(res_words, str) - assert isinstance(res_hex, str) - assert res_hex == '0x9e876134d90499dd' - assert res_words == 'INCH SEA ANNE LONG AHEM TOUR' + response = gen.generate_otp_response(0) + assert isinstance(response, otp2289.OTPResponse) + assert isinstance(response.words, str) + assert isinstance(response.hexdigest, str) + assert response.hexdigest == '0x9e876134d90499dd' + assert response.words == 'INCH SEA ANNE LONG AHEM TOUR' + # step 1 - assert gen.generate_otp_hexdigest(1) == '0x7965e05436f5029f' - assert gen.generate_otp_words(1) == 'EASE OIL FUM CURE AWRY AVIS' - assert gen.generate_otp_hexdigest_from_challenge('otp-md5 1 TeSt') == ( - '0x7965e05436f5029f' + response = gen.generate_otp_response(1) + assert response.hexdigest == '0x7965e05436f5029f' + assert response.words == 'EASE OIL FUM CURE AWRY AVIS' + assert ( + gen.generate_otp_response_from_challenge('otp-md5 1 TeSt').hexdigest + == '0x7965e05436f5029f' ) - assert gen.generate_otp_words_from_challenge('otp-md5 1 TeSt') == ( - 'EASE OIL FUM CURE AWRY AVIS' + assert ( + gen.generate_otp_response_from_challenge('otp-md5 1 TeSt').words + == 'EASE OIL FUM CURE AWRY AVIS' ) + # step 99 - assert gen.generate_otp_hexdigest(99) == '0x50fe1962c4965880' - assert gen.generate_otp_words(99) == 'BAIL TUFT BITS GANG CHEF THY' - assert gen.generate_otp_hexdigest_from_challenge('otp-md5 99 TeSt') == ( - '0x50fe1962c4965880' + response = gen.generate_otp_response(99) + assert response.hexdigest == '0x50fe1962c4965880' + assert response.words == 'BAIL TUFT BITS GANG CHEF THY' + assert ( + gen.generate_otp_response_from_challenge('otp-md5 99 TeSt').hexdigest + == '0x50fe1962c4965880' ) - assert gen.generate_otp_words_from_challenge('otp-md5 99 TeSt') == ( - 'BAIL TUFT BITS GANG CHEF THY' + assert ( + gen.generate_otp_response_from_challenge('otp-md5 99 TeSt').words + == 'BAIL TUFT BITS GANG CHEF THY' ) + # iterator test - hexdigests = list(gen.hexdigest_range(105)) # testing the range itself - words = list(gen.words_range(99)) + hexdigests = list(gen.otp_response_range(105)) # testing the range itself + words = list(gen.otp_response_range(99)) hexdigests.reverse() words.reverse() - assert hexdigests[0] == '0x9e876134d90499dd' - assert hexdigests[1] == '0x7965e05436f5029f' - assert hexdigests[99] == '0x50fe1962c4965880' - assert words[0] == 'INCH SEA ANNE LONG AHEM TOUR' - assert words[1] == 'EASE OIL FUM CURE AWRY AVIS' - assert words[99] == 'BAIL TUFT BITS GANG CHEF THY' + assert hexdigests[0].hexdigest == '0x9e876134d90499dd' + assert hexdigests[1].hexdigest == '0x7965e05436f5029f' + assert hexdigests[99].hexdigest == '0x50fe1962c4965880' + assert words[0].words == 'INCH SEA ANNE LONG AHEM TOUR' + assert words[1].words == 'EASE OIL FUM CURE AWRY AVIS' + assert words[99].words == 'BAIL TUFT BITS GANG CHEF THY' + # pass='AbCdEfGhIjK', seed='alpha1' gen = otp2289.OTPGenerator(b'AbCdEfGhIjK', 'alpha1', otp2289.OTP_ALGO_MD5) - assert gen.generate_otp_hexdigest(0) == '0x87066dd9644bf206' - assert gen.generate_otp_words(0) == 'FULL PEW DOWN ONCE MORT ARC' - assert gen.generate_otp_hexdigest(1) == '0x7cd34c1040add14b' - assert gen.generate_otp_words(1) == 'FACT HOOF AT FIST SITE KENT' - assert gen.generate_otp_hexdigest(99) == '0x5aa37a81f212146c' - assert gen.generate_otp_words(99) == 'BODE HOP JAKE STOW JUT RAP' + response = gen.generate_otp_response(0) + assert response.hexdigest == '0x87066dd9644bf206' + assert response.words == 'FULL PEW DOWN ONCE MORT ARC' + response = gen.generate_otp_response(1) + assert response.hexdigest == '0x7cd34c1040add14b' + assert response.words == 'FACT HOOF AT FIST SITE KENT' + response = gen.generate_otp_response(99) + assert response.hexdigest == '0x5aa37a81f212146c' + assert response.words == 'BODE HOP JAKE STOW JUT RAP' + # pass="OTP's are good", seed='correct' gen = otp2289.OTPGenerator( b"OTP's are good", 'correct', otp2289.OTP_ALGO_MD5 ) - assert gen.generate_otp_hexdigest(0) == '0xf205753943de4cf9' - assert gen.generate_otp_words(0) == 'ULAN NEW ARMY FUSE SUIT EYED' - assert gen.generate_otp_hexdigest(1) == '0xddcdac956f234937' - assert gen.generate_otp_words(1) == 'SKIM CULT LOB SLAM POE HOWL' - assert gen.generate_otp_hexdigest(99) == '0xb203e28fa525be47' - assert gen.generate_otp_words(99) == 'LONG IVY JULY AJAR BOND LEE' + response = gen.generate_otp_response(0) + assert response.hexdigest == '0xf205753943de4cf9' + assert response.words == 'ULAN NEW ARMY FUSE SUIT EYED' + response = gen.generate_otp_response(1) + assert response.hexdigest == '0xddcdac956f234937' + assert response.words == 'SKIM CULT LOB SLAM POE HOWL' + response = gen.generate_otp_response(99) + assert response.hexdigest == '0xb203e28fa525be47' + assert response.words == 'LONG IVY JULY AJAR BOND LEE' + + +def test_otp_response() -> None: + """Tests OTPResponse""" + gen = otp2289.OTPGenerator( + b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5 + ) + response = gen.generate_otp_response(0) + assert response.response_bytes == bytes(response) + + # From 'RFC-2289 Appendix C - OTP Verification Examples' + # hexdigest: 9e876134d90499dd + # words: INCH SEA ANNE LONG AHEM TOUR + response_from_tokens = otp2289.OTPResponse.from_tokens( + 'INCH SEA ANNE LONG AHEM TOUR' + ) + response_from_hex1 = otp2289.OTPResponse.from_hex('9e876134d90499dd') + response_from_hex2 = otp2289.OTPResponse.from_hex('0x9e876134d90499dd') + assert ( + response_from_tokens.response_bytes + == response_from_hex1.response_bytes + ) + assert ( + response_from_tokens.response_bytes + == response_from_hex2.response_bytes + ) + + # same tests, only using __eq__ + assert response_from_tokens == response_from_hex1 + assert response_from_tokens == response_from_hex2 + + +def test_otp_response_exceptions() -> None: + """Tests OTPResponse exceptions""" + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_hex( + b'9e876134d90499dd' # ty: ignore[invalid-argument-type] + ) + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == 'OT-hex must be a str' + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_hex('9e876134d90499d') + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == ( + 'The length of the hex should be 16 (representing 64 bits digest)' + ) + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_hex('9e876134d90499dg') + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == 'Invalid OT-hex' + + # .from_tokens + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_tokens( + b'INCH SEA ANNE LONG AHEM TOUR' # ty: ignore[invalid-argument-type] + ) + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == 'tokens must be a str' + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_tokens('INCH SEA ANNE LONG AHEM') + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == 'Tokens-string does not contain 6 tokens' + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_tokens('INCH SEA ANNE LONG AHEM SIMEON') + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == ( + 'One or more words not present in RFC1760' + ) + with pytest.raises(otp2289.OTPResponseError) as exc_info: + otp2289.OTPResponse.from_tokens('INCH SEA ANNE LONG AHEM LEE') + assert exc_info.type is otp2289.OTPResponseError + assert exc_info.value.args[0] == 'Invalid bit checksum' def test_sha1() -> None: @@ -198,54 +287,71 @@ def test_sha1() -> None: gen = otp2289.OTPGenerator( b'This is a test.', 'TeSt', otp2289.OTP_ALGO_SHA1 ) - res_hex = gen.generate_otp_hexdigest(step=0) - res_words = gen.generate_otp_words(step=0) - assert isinstance(res_words, str) - assert isinstance(res_hex, str) - assert res_hex == '0xbb9e6ae1979d8ff4' - assert res_words == 'MILT VARY MAST OK SEES WENT' - assert gen.generate_otp_hexdigest(1) == '0x63d936639734385b' - assert gen.generate_otp_words(1) == 'CART OTTO HIVE ODE VAT NUT' - assert gen.generate_otp_hexdigest_from_challenge('otp-sha1 1 TeSt') == ( - '0x63d936639734385b' + response = gen.generate_otp_response(step=0) + assert isinstance(response, otp2289.OTPResponse) + assert isinstance(response.words, str) + assert isinstance(response.hexdigest, str) + assert response.hexdigest == '0xbb9e6ae1979d8ff4' + assert response.words == 'MILT VARY MAST OK SEES WENT' + response = gen.generate_otp_response(1) + assert response.hexdigest == '0x63d936639734385b' + assert response.words == 'CART OTTO HIVE ODE VAT NUT' + assert ( + gen.generate_otp_response_from_challenge('otp-sha1 1 TeSt').hexdigest + == '0x63d936639734385b' ) - assert gen.generate_otp_words_from_challenge('otp-sha1 1 TeSt') == ( - 'CART OTTO HIVE ODE VAT NUT' + assert ( + gen.generate_otp_response_from_challenge('otp-sha1 1 TeSt').words + == 'CART OTTO HIVE ODE VAT NUT' ) - assert gen.generate_otp_hexdigest(99) == '0x87fec7768b73ccf9' - assert gen.generate_otp_words(99) == 'GAFF WAIT SKID GIG SKY EYED' - assert gen.generate_otp_hexdigest_from_challenge('otp-sha1 99 TeSt') == ( - '0x87fec7768b73ccf9' + response = gen.generate_otp_response(99) + assert response.hexdigest == '0x87fec7768b73ccf9' + assert response.words == 'GAFF WAIT SKID GIG SKY EYED' + assert ( + gen.generate_otp_response_from_challenge( + 'otp-sha1 99 TeSt' + ).hexdigest + == '0x87fec7768b73ccf9' ) - assert gen.generate_otp_words_from_challenge('otp-sha1 99 TeSt') == ( - 'GAFF WAIT SKID GIG SKY EYED' + assert ( + gen.generate_otp_response_from_challenge('otp-sha1 99 TeSt').words + == 'GAFF WAIT SKID GIG SKY EYED' ) + # iterator test - hexdigests = list(gen.hexdigest_range(105)) - words = list(gen.words_range(99)) + hexdigests = list(gen.otp_response_range(105)) + words = list(gen.otp_response_range(99)) hexdigests.reverse() words.reverse() - assert hexdigests[0] == '0xbb9e6ae1979d8ff4' - assert hexdigests[1] == '0x63d936639734385b' - assert hexdigests[99] == '0x87fec7768b73ccf9' - assert words[0] == 'MILT VARY MAST OK SEES WENT' - assert words[1] == 'CART OTTO HIVE ODE VAT NUT' - assert words[99] == 'GAFF WAIT SKID GIG SKY EYED' + assert hexdigests[0].hexdigest == '0xbb9e6ae1979d8ff4' + assert hexdigests[1].hexdigest == '0x63d936639734385b' + assert hexdigests[99].hexdigest == '0x87fec7768b73ccf9' + assert words[0].words == 'MILT VARY MAST OK SEES WENT' + assert words[1].words == 'CART OTTO HIVE ODE VAT NUT' + assert words[99].words == 'GAFF WAIT SKID GIG SKY EYED' + # pass='AbCdEfGhIjK', seed='alpha1' gen = otp2289.OTPGenerator(b'AbCdEfGhIjK', 'alpha1', otp2289.OTP_ALGO_SHA1) - assert gen.generate_otp_hexdigest(0) == '0xad85f658ebe383c9' - assert gen.generate_otp_words(0) == 'LEST OR HEEL SCOT ROB SUIT' - assert gen.generate_otp_hexdigest(1) == '0xd07ce229b5cf119b' - assert gen.generate_otp_words(1) == 'RITE TAKE GELD COST TUNE RECK' - assert gen.generate_otp_hexdigest(99) == '0x27bc71035aaf3dc6' - assert gen.generate_otp_words(99) == 'MAY STAR TIN LYON VEDA STAN' + response = gen.generate_otp_response(0) + assert response.hexdigest == '0xad85f658ebe383c9' + assert response.words == 'LEST OR HEEL SCOT ROB SUIT' + response = gen.generate_otp_response(1) + assert response.hexdigest == '0xd07ce229b5cf119b' + assert response.words == 'RITE TAKE GELD COST TUNE RECK' + response = gen.generate_otp_response(99) + assert response.hexdigest == '0x27bc71035aaf3dc6' + assert response.words == 'MAY STAR TIN LYON VEDA STAN' + # pass="OTP's are good", seed='correct' gen = otp2289.OTPGenerator( b"OTP's are good", 'correct', otp2289.OTP_ALGO_SHA1 ) - assert gen.generate_otp_hexdigest(0) == '0xd51f3e99bf8e6f0b' - assert gen.generate_otp_words(0) == 'RUST WELT KICK FELL TAIL FRAU' - assert gen.generate_otp_hexdigest(1) == '0x82aeb52d943774e4' - assert gen.generate_otp_words(1) == 'FLIT DOSE ALSO MEW DRUM DEFY' - assert gen.generate_otp_hexdigest(99) == '0x4f296a74fe1567ec' - assert gen.generate_otp_words(99) == 'AURA ALOE HURL WING BERG WAIT' + response = gen.generate_otp_response(0) + assert response.hexdigest == '0xd51f3e99bf8e6f0b' + assert response.words == 'RUST WELT KICK FELL TAIL FRAU' + response = gen.generate_otp_response(1) + assert response.hexdigest == '0x82aeb52d943774e4' + assert response.words == 'FLIT DOSE ALSO MEW DRUM DEFY' + response = gen.generate_otp_response(99) + assert response.hexdigest == '0x4f296a74fe1567ec' + assert response.words == 'AURA ALOE HURL WING BERG WAIT' -- cgit v1.3