summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_generator.py251
-rw-r--r--tests/test_main.py225
-rw-r--r--tests/test_server.py121
-rw-r--r--tests/test_static.py95
4 files changed, 692 insertions, 0 deletions
diff --git a/tests/test_generator.py b/tests/test_generator.py
new file mode 100644
index 0000000..08947e5
--- /dev/null
+++ b/tests/test_generator.py
@@ -0,0 +1,251 @@
1# SPDX-License-Identifier: BSD-2-Clause
2#
3# Copyright (c) 2020-2026, Simeon Simeonov
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright notice,
12# this list of conditions and the following disclaimer in the documentation
13# and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
16# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25"""Tests for otp2289.generator"""
26
27import pytest
28
29import otp2289
30
31
32def test_caller_exceptions() -> None:
33 """Tests the exceptions when calling an initialized object"""
34 gen = otp2289.OTPGenerator(
35 b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5
36 )
37 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
38 gen.generate_otp_words('3') # ty: ignore[invalid-argument-type]
39 assert exc_info.type is otp2289.OTPGeneratorError
40 assert exc_info.value.args[0] == 'Step value MUST be an int'
41 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
42 gen.generate_otp_hexdigest(-1)
43 assert exc_info.type is otp2289.OTPGeneratorError
44 assert exc_info.value.args[0] == 'Step value MUST be >= 0'
45 with pytest.raises(otp2289.OTPChallengeError) as exc_info:
46 gen.generate_otp_hexdigest_from_challenge(
47 b'md5 fbd TeSt' # ty: ignore[invalid-argument-type]
48 )
49 assert exc_info.type is otp2289.OTPChallengeError
50 assert exc_info.value.args[0] == 'Challenge must be str'
51 with pytest.raises(otp2289.OTPChallengeError) as exc_info:
52 gen.generate_otp_hexdigest_from_challenge('md5 fbd TeSt')
53 assert exc_info.type is otp2289.OTPChallengeError
54 assert exc_info.value.args[0] == 'Invalid challenge'
55 with pytest.raises(otp2289.generator.OTPChallengeError) as exc_info:
56 gen.generate_otp_hexdigest_from_challenge('otp-md5 fbd TeSt')
57 assert exc_info.type is otp2289.generator.OTPChallengeError
58 assert exc_info.value.args[0] == 'Invalid challenge'
59
60
61def test_constructor_exceptions() -> None:
62 """
63 Tests the exceptions when initializing a new object (in the constructor)
64 """
65 # test the otp2289.OTPGenerator __init__ and validators
66 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
67 otp2289.OTPGenerator(
68 b'This is a test.',
69 'TeStø'.encode(), # ty: ignore[invalid-argument-type]
70 otp2289.OTP_ALGO_MD5,
71 )
72 assert exc_info.type is otp2289.OTPGeneratorError
73 assert exc_info.value.args[0] == 'Seed must be a string'
74 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
75 otp2289.OTPGenerator(
76 b'This is a test.', 'TeStøtEsTteSTteStTest', otp2289.OTP_ALGO_SHA1
77 )
78 assert exc_info.type is otp2289.OTPGeneratorError
79 assert exc_info.value.args[0] == (
80 'The seed MUST be of 1 to 16 characters in length'
81 )
82 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
83 otp2289.OTPGenerator(
84 b'This is a test.', 'TeStø', otp2289.OTP_ALGO_SHA1
85 )
86 assert exc_info.type is otp2289.OTPGeneratorError
87 assert exc_info.value.args[0] == (
88 'The seed MUST consist of purely alphanumeric characters'
89 )
90 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
91 otp2289.OTPGenerator(b'This is a test.', 'TeSt', 9)
92 assert exc_info.type is otp2289.OTPGeneratorError
93 assert exc_info.value.args[0] == (
94 'hash_algo is not among the known algorithms'
95 )
96 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
97 otp2289.OTPGenerator(
98 b'This is a test.',
99 'TeSt',
100 b'md5', # ty: ignore[invalid-argument-type]
101 )
102 assert exc_info.type is otp2289.OTPGeneratorError
103 assert exc_info.value.args[0] == 'hash_algo must be an int or a str'
104 # test the package structure as well
105 with pytest.raises(otp2289.generator.OTPGeneratorError) as exc_info:
106 otp2289.generator.OTPGenerator(b'This is a test.', 'TeSt', 'foo')
107 assert exc_info.type is otp2289.generator.OTPGeneratorError
108 assert exc_info.value.args[0] == (
109 'foo is not supported by this version of the hashlib module'
110 )
111 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
112 otp2289.OTPGenerator(
113 '1234567', # ty: ignore[invalid-argument-type]
114 'TeSt',
115 otp2289.OTP_ALGO_MD5,
116 )
117 assert exc_info.type is otp2289.OTPGeneratorError
118 assert exc_info.value.args[0] == 'Password must be a byte-string'
119 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
120 otp2289.OTPGenerator(b'1234567', 'TeSt', otp2289.OTP_ALGO_MD5)
121 assert exc_info.type is otp2289.OTPGeneratorError
122 assert exc_info.value.args[0] == 'Password must be longer than 10 bytes'
123
124
125def test_md5() -> None:
126 """
127 Tests the MD5 functionality of the OTPGenerator as described in the RFC
128
129 Those are the tests from 'RFC-2289 Appendix C - OTP Verification Examples'
130 """
131 # We could run this in a loop, but I guess "Readability counts."
132 # pass='This is a test.', seed='TeSt'
133 gen = otp2289.OTPGenerator(
134 b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5
135 )
136 res_words = gen.generate_otp_words(0)
137 res_hex = gen.generate_otp_hexdigest(0)
138 assert isinstance(res_words, str)
139 assert isinstance(res_hex, str)
140 assert res_hex == '0x9e876134d90499dd'
141 assert res_words == 'INCH SEA ANNE LONG AHEM TOUR'
142 # step 1
143 assert gen.generate_otp_hexdigest(1) == '0x7965e05436f5029f'
144 assert gen.generate_otp_words(1) == 'EASE OIL FUM CURE AWRY AVIS'
145 assert gen.generate_otp_hexdigest_from_challenge('otp-md5 1 TeSt') == (
146 '0x7965e05436f5029f'
147 )
148 assert gen.generate_otp_words_from_challenge('otp-md5 1 TeSt') == (
149 'EASE OIL FUM CURE AWRY AVIS'
150 )
151 # step 99
152 assert gen.generate_otp_hexdigest(99) == '0x50fe1962c4965880'
153 assert gen.generate_otp_words(99) == 'BAIL TUFT BITS GANG CHEF THY'
154 assert gen.generate_otp_hexdigest_from_challenge('otp-md5 99 TeSt') == (
155 '0x50fe1962c4965880'
156 )
157 assert gen.generate_otp_words_from_challenge('otp-md5 99 TeSt') == (
158 'BAIL TUFT BITS GANG CHEF THY'
159 )
160 # iterator test
161 hexdigests = list(gen.hexdigest_range(105)) # testing the range itself
162 words = list(gen.words_range(99))
163 hexdigests.reverse()
164 words.reverse()
165 assert hexdigests[0] == '0x9e876134d90499dd'
166 assert hexdigests[1] == '0x7965e05436f5029f'
167 assert hexdigests[99] == '0x50fe1962c4965880'
168 assert words[0] == 'INCH SEA ANNE LONG AHEM TOUR'
169 assert words[1] == 'EASE OIL FUM CURE AWRY AVIS'
170 assert words[99] == 'BAIL TUFT BITS GANG CHEF THY'
171 # pass='AbCdEfGhIjK', seed='alpha1'
172 gen = otp2289.OTPGenerator(b'AbCdEfGhIjK', 'alpha1', otp2289.OTP_ALGO_MD5)
173 assert gen.generate_otp_hexdigest(0) == '0x87066dd9644bf206'
174 assert gen.generate_otp_words(0) == 'FULL PEW DOWN ONCE MORT ARC'
175 assert gen.generate_otp_hexdigest(1) == '0x7cd34c1040add14b'
176 assert gen.generate_otp_words(1) == 'FACT HOOF AT FIST SITE KENT'
177 assert gen.generate_otp_hexdigest(99) == '0x5aa37a81f212146c'
178 assert gen.generate_otp_words(99) == 'BODE HOP JAKE STOW JUT RAP'
179 # pass="OTP's are good", seed='correct'
180 gen = otp2289.OTPGenerator(
181 b"OTP's are good", 'correct', otp2289.OTP_ALGO_MD5
182 )
183 assert gen.generate_otp_hexdigest(0) == '0xf205753943de4cf9'
184 assert gen.generate_otp_words(0) == 'ULAN NEW ARMY FUSE SUIT EYED'
185 assert gen.generate_otp_hexdigest(1) == '0xddcdac956f234937'
186 assert gen.generate_otp_words(1) == 'SKIM CULT LOB SLAM POE HOWL'
187 assert gen.generate_otp_hexdigest(99) == '0xb203e28fa525be47'
188 assert gen.generate_otp_words(99) == 'LONG IVY JULY AJAR BOND LEE'
189
190
191def test_sha1() -> None:
192 """
193 Tests the SHA-1 functionality of the OTPGenerator as described in the RFC
194
195 Those are the tests from 'RFC-2289 Appendix C - OTP Verification Examples'
196 """
197 # pass='This is a test.', seed='TeSt'
198 gen = otp2289.OTPGenerator(
199 b'This is a test.', 'TeSt', otp2289.OTP_ALGO_SHA1
200 )
201 res_hex = gen.generate_otp_hexdigest(step=0)
202 res_words = gen.generate_otp_words(step=0)
203 assert isinstance(res_words, str)
204 assert isinstance(res_hex, str)
205 assert res_hex == '0xbb9e6ae1979d8ff4'
206 assert res_words == 'MILT VARY MAST OK SEES WENT'
207 assert gen.generate_otp_hexdigest(1) == '0x63d936639734385b'
208 assert gen.generate_otp_words(1) == 'CART OTTO HIVE ODE VAT NUT'
209 assert gen.generate_otp_hexdigest_from_challenge('otp-sha1 1 TeSt') == (
210 '0x63d936639734385b'
211 )
212 assert gen.generate_otp_words_from_challenge('otp-sha1 1 TeSt') == (
213 'CART OTTO HIVE ODE VAT NUT'
214 )
215 assert gen.generate_otp_hexdigest(99) == '0x87fec7768b73ccf9'
216 assert gen.generate_otp_words(99) == 'GAFF WAIT SKID GIG SKY EYED'
217 assert gen.generate_otp_hexdigest_from_challenge('otp-sha1 99 TeSt') == (
218 '0x87fec7768b73ccf9'
219 )
220 assert gen.generate_otp_words_from_challenge('otp-sha1 99 TeSt') == (
221 'GAFF WAIT SKID GIG SKY EYED'
222 )
223 # iterator test
224 hexdigests = list(gen.hexdigest_range(105))
225 words = list(gen.words_range(99))
226 hexdigests.reverse()
227 words.reverse()
228 assert hexdigests[0] == '0xbb9e6ae1979d8ff4'
229 assert hexdigests[1] == '0x63d936639734385b'
230 assert hexdigests[99] == '0x87fec7768b73ccf9'
231 assert words[0] == 'MILT VARY MAST OK SEES WENT'
232 assert words[1] == 'CART OTTO HIVE ODE VAT NUT'
233 assert words[99] == 'GAFF WAIT SKID GIG SKY EYED'
234 # pass='AbCdEfGhIjK', seed='alpha1'
235 gen = otp2289.OTPGenerator(b'AbCdEfGhIjK', 'alpha1', otp2289.OTP_ALGO_SHA1)
236 assert gen.generate_otp_hexdigest(0) == '0xad85f658ebe383c9'
237 assert gen.generate_otp_words(0) == 'LEST OR HEEL SCOT ROB SUIT'
238 assert gen.generate_otp_hexdigest(1) == '0xd07ce229b5cf119b'
239 assert gen.generate_otp_words(1) == 'RITE TAKE GELD COST TUNE RECK'
240 assert gen.generate_otp_hexdigest(99) == '0x27bc71035aaf3dc6'
241 assert gen.generate_otp_words(99) == 'MAY STAR TIN LYON VEDA STAN'
242 # pass="OTP's are good", seed='correct'
243 gen = otp2289.OTPGenerator(
244 b"OTP's are good", 'correct', otp2289.OTP_ALGO_SHA1
245 )
246 assert gen.generate_otp_hexdigest(0) == '0xd51f3e99bf8e6f0b'
247 assert gen.generate_otp_words(0) == 'RUST WELT KICK FELL TAIL FRAU'
248 assert gen.generate_otp_hexdigest(1) == '0x82aeb52d943774e4'
249 assert gen.generate_otp_words(1) == 'FLIT DOSE ALSO MEW DRUM DEFY'
250 assert gen.generate_otp_hexdigest(99) == '0x4f296a74fe1567ec'
251 assert gen.generate_otp_words(99) == 'AURA ALOE HURL WING BERG WAIT'
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..3c75621
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,225 @@
1# SPDX-License-Identifier: BSD-2-Clause
2#
3# Copyright (c) 2020-2026, Simeon Simeonov
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright notice,
12# this list of conditions and the following disclaimer in the documentation
13# and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
16# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25"""Tests for otp2289.__main__"""
26
27import os
28import unittest.mock
29
30import pytest
31
32from otp2289.__main__ import main
33
34
35def test_main_generate_otp_response(capsys: pytest.CaptureFixture) -> None:
36 """tests main"""
37 args = [
38 '--generate-otp-response',
39 '-a',
40 'sha1',
41 '-i',
42 '99',
43 '-s',
44 'TesT',
45 '-p',
46 'This is a test.',
47 ]
48 with pytest.raises(SystemExit) as exit_info:
49 main(args)
50 captured = capsys.readouterr()
51 assert captured.out == (
52 f'Seed: TesT, Step: 99, Hash: sha1{os.linesep}'
53 f'0x87fec7768b73ccf9{os.linesep}'
54 )
55 assert exit_info.value.code == 0
56 args.extend(['-f', 'token'])
57 with pytest.raises(SystemExit) as exit_info:
58 main(args)
59 captured = capsys.readouterr()
60 assert captured.out == (
61 f'Seed: TesT, Step: 99, Hash: sha1{os.linesep}'
62 f'GAFF WAIT SKID GIG SKY EYED{os.linesep}'
63 )
64 assert exit_info.value.code == 0
65 args.append('-q')
66 with pytest.raises(SystemExit) as exit_info:
67 main(args)
68 captured = capsys.readouterr()
69 assert captured.out == f'GAFF WAIT SKID GIG SKY EYED{os.linesep}'
70 assert exit_info.value.code == 0
71
72
73def test_main_generate_otp_response_env_passwd(
74 capsys: pytest.CaptureFixture,
75) -> None:
76 """tests main by fetching password from the env. var. 'OTP2289_PASSWORD'"""
77 args = ['--generate-otp-response', '-a', 'sha1', '-i', '99', '-s', 'TesT']
78 with unittest.mock.patch.dict(
79 os.environ, {'OTP2289_PASSWORD': 'This is a test.'}
80 ):
81 with pytest.raises(SystemExit) as exit_info:
82 main(args)
83 captured = capsys.readouterr()
84 assert captured.out == (
85 f'Seed: TesT, Step: 99, Hash: sha1{os.linesep}'
86 f'0x87fec7768b73ccf9{os.linesep}'
87 )
88 assert exit_info.value.code == 0
89 args.extend(['-f', 'token'])
90 with pytest.raises(SystemExit) as exit_info:
91 main(args)
92 captured = capsys.readouterr()
93 assert captured.out == (
94 f'Seed: TesT, Step: 99, Hash: sha1{os.linesep}'
95 f'GAFF WAIT SKID GIG SKY EYED{os.linesep}'
96 )
97 assert exit_info.value.code == 0
98 args.append('-q')
99 with pytest.raises(SystemExit) as exit_info:
100 main(args)
101 captured = capsys.readouterr()
102 assert captured.out == f'GAFF WAIT SKID GIG SKY EYED{os.linesep}'
103 assert exit_info.value.code == 0
104
105
106def test_main_generate_otp_range(capsys: pytest.CaptureFixture) -> None:
107 """tests main"""
108 args = [
109 '--generate-otp-range',
110 '-i',
111 '2',
112 '-s',
113 'TesT',
114 '-r',
115 '5',
116 '-p',
117 'This is a test.',
118 ]
119 with pytest.raises(SystemExit) as exit_info:
120 main(args)
121 captured = capsys.readouterr()
122 assert captured.out == (
123 f'Seed: TesT, Step: 2, Hash: md5, Range: 3'
124 f'{os.linesep}'
125 f'2: 0x4049f8b161669b7b{os.linesep}'
126 f'1: 0x7965e05436f5029f{os.linesep}'
127 f'0: 0x9e876134d90499dd{os.linesep}'
128 )
129 assert exit_info.value.code == 0
130 args.append('-q')
131 with pytest.raises(SystemExit) as exit_info:
132 main(args)
133 captured = capsys.readouterr()
134 assert captured.out == (
135 f'2: 0x4049f8b161669b7b{os.linesep}'
136 f'1: 0x7965e05436f5029f{os.linesep}'
137 f'0: 0x9e876134d90499dd{os.linesep}'
138 )
139 assert exit_info.value.code == 0
140 args.extend(['-f', 'token'])
141 with pytest.raises(SystemExit) as exit_info:
142 main(args)
143 captured = capsys.readouterr()
144 assert captured.out == (
145 f'2: THY AVON NO NECK COKE MOLL{os.linesep}'
146 f'1: EASE OIL FUM CURE AWRY AVIS{os.linesep}'
147 f'0: INCH SEA ANNE LONG AHEM TOUR{os.linesep}'
148 )
149 assert exit_info.value.code == 0
150
151
152@pytest.mark.parametrize(
153 'args',
154 [
155 ['--generate-otp-range', '-i', '2', '-s', 'TesT', '-r', '5'],
156 ['--generate-otp-range', '-i', '2', '-s', 'TesT', '-r', '5', '-P'],
157 ],
158)
159@unittest.mock.patch('getpass.getpass')
160def test_main_generate_otp_range_passwd_prompt(
161 getpass: unittest.mock.MagicMock,
162 capsys: pytest.CaptureFixture,
163 args: list[str],
164) -> None:
165 """tests main by prompting for password (with or without -P)"""
166 args = ['--generate-otp-range', '-i', '2', '-s', 'TesT', '-r', '5']
167 getpass.return_value = 'This is a test.'
168 with pytest.raises(SystemExit) as exit_info:
169 main(args)
170 captured = capsys.readouterr()
171 assert captured.out == (
172 f'Seed: TesT, Step: 2, Hash: md5, Range: 3'
173 f'{os.linesep}'
174 f'2: 0x4049f8b161669b7b{os.linesep}'
175 f'1: 0x7965e05436f5029f{os.linesep}'
176 f'0: 0x9e876134d90499dd{os.linesep}'
177 )
178 assert exit_info.value.code == 0
179 args.append('-q')
180 with pytest.raises(SystemExit) as exit_info:
181 main(args)
182 captured = capsys.readouterr()
183 assert captured.out == (
184 f'2: 0x4049f8b161669b7b{os.linesep}'
185 f'1: 0x7965e05436f5029f{os.linesep}'
186 f'0: 0x9e876134d90499dd{os.linesep}'
187 )
188 assert exit_info.value.code == 0
189 args.extend(['-f', 'token'])
190 with pytest.raises(SystemExit) as exit_info:
191 main(args)
192 captured = capsys.readouterr()
193 assert captured.out == (
194 f'2: THY AVON NO NECK COKE MOLL{os.linesep}'
195 f'1: EASE OIL FUM CURE AWRY AVIS{os.linesep}'
196 f'0: INCH SEA ANNE LONG AHEM TOUR{os.linesep}'
197 )
198 assert exit_info.value.code == 0
199
200
201def test_main_initiate(capsys: pytest.CaptureFixture) -> None:
202 """tests main"""
203 args = [
204 '--initiate-new-sequence',
205 '-i',
206 '500',
207 '-s',
208 'TesT',
209 '-p',
210 'This is a test.',
211 ]
212 with pytest.raises(SystemExit) as exit_info:
213 main(args)
214 captured = capsys.readouterr()
215 assert captured.out == (
216 f'Seed: TesT, Step: 500, Hash: md5{os.linesep}'
217 f'0x2b8d82b6ac14346c{os.linesep}'
218 )
219 assert exit_info.value.code == 0
220 args.append('-q')
221 with pytest.raises(SystemExit) as exit_info:
222 main(args)
223 captured = capsys.readouterr()
224 assert captured.out == f'0x2b8d82b6ac14346c{os.linesep}'
225 assert exit_info.value.code == 0
diff --git a/tests/test_server.py b/tests/test_server.py
new file mode 100644
index 0000000..3099cf7
--- /dev/null
+++ b/tests/test_server.py
@@ -0,0 +1,121 @@
1# SPDX-License-Identifier: BSD-2-Clause
2#
3# Copyright (c) 2020-2026, Simeon Simeonov
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright notice,
12# this list of conditions and the following disclaimer in the documentation
13# and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
16# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25"""Tests for otp2289.server"""
26
27import json
28
29import pytest
30
31import otp2289
32
33
34def test_state_caller_exceptions() -> None:
35 """Tests the exceptions when calling the OTPState objects"""
36 state = otp2289.OTPState(
37 '0x7965e05436f5029f', 1, 'TeSt', otp2289.OTP_ALGO_MD5
38 )
39 with pytest.raises(otp2289.OTPInvalidResponseError) as exc_info:
40 state.response_validates('bla')
41 assert exc_info.type is otp2289.OTPInvalidResponseError
42 assert exc_info.value.args[0] == (
43 'The response is neither a valid token or hex'
44 )
45
46
47def test_state_constructor_exceptions() -> None:
48 """Tests the exceptions when initializing new OTPState objects"""
49 with pytest.raises(otp2289.OTPStateError) as exc_info:
50 otp2289.OTPState(
51 '0x7965e05436f5029t',
52 1,
53 'TeStø'.encode(), # ty: ignore[invalid-argument-type]
54 otp2289.OTP_ALGO_MD5,
55 )
56 assert exc_info.type is otp2289.OTPStateError
57 assert exc_info.value.args[0] == 'Seed must be a string'
58
59 with pytest.raises(otp2289.OTPStateError) as exc_info:
60 otp2289.OTPState(
61 '0x7965e05436f5029t',
62 '1', # ty: ignore[invalid-argument-type]
63 'TeSt',
64 otp2289.OTP_ALGO_MD5,
65 )
66 assert exc_info.type is otp2289.OTPStateError
67 assert exc_info.value.args[0] == 'Step value MUST be an int'
68
69
70def test_state_validation_md5() -> None:
71 """Tests the OTPState validation functionality for MD5"""
72 state = otp2289.OTPState(
73 '0x7965e05436f5029f', 1, 'TeSt', otp2289.OTP_ALGO_MD5
74 )
75 assert state.validated is False
76 assert state.response_validates('0x9e876134d90499dd') is True
77 assert state.response_validates('INCH SEA ANNE LONG AHEM TOUR') is True
78 assert state.ot_hex == '7965e05436f5029f'
79 assert state.validated is True
80
81
82def test_state_validation_sha1() -> None:
83 """Tests the OTPState validation functionality for SHA1"""
84 state = otp2289.OTPState(
85 '0x63d936639734385b', 1, 'TeSt', otp2289.OTP_ALGO_SHA1
86 )
87 assert state.validated is False
88 assert state.response_validates('0xbb9e6ae1979d8ff4') is True
89 assert state.response_validates('MILT VARY MAST OK SEES WENT') is True
90 assert state.ot_hex == '63d936639734385b'
91 assert state.validated is True
92
93
94def test_store() -> None:
95 """Tests the OTPStore functionality"""
96 store_data = {
97 'sgs': {
98 'ot_hex': '0x7965e05436f5029f',
99 'current_step': 1,
100 'seed': 'TeSt',
101 'hash_algo': 'md5',
102 },
103 'blackmore': {
104 'ot_hex': '0x63d936639734385b',
105 'current_step': 1,
106 'seed': 'TeSt',
107 'hash_algo': 'sha1',
108 },
109 }
110 store = otp2289.OTPStore(store_data)
111 assert len(store) == len(store_data)
112 assert isinstance(json.dumps(store.to_dict()), str) # serializable?
113 assert store.response_validates('sgs', '0x9e876134d90499dd') is True
114 assert store.response_validates('sgs', '0x9e876134d90499dd') is False
115 sgs_state = store.get('sgs')
116 if sgs_state is not None:
117 assert sgs_state in store
118 store.pop_state('sgs')
119 assert bool(store) is True
120 store.pop_state('blackmore')
121 assert bool(store) is False
diff --git a/tests/test_static.py b/tests/test_static.py
new file mode 100644
index 0000000..589fc17
--- /dev/null
+++ b/tests/test_static.py
@@ -0,0 +1,95 @@
1# SPDX-License-Identifier: BSD-2-Clause
2#
3# Copyright (c) 2020-2026, Simeon Simeonov
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright notice,
12# this list of conditions and the following disclaimer in the documentation
13# and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
16# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25"""Tests for the static methods and basic bit, byte, token functionality"""
26
27import os
28
29import otp2289
30
31
32def test_bytes_and_tokens() -> None:
33 """Tests the official hex and tokens defined in RFC2289"""
34 assert bytes.fromhex('9e876134d90499dd') == (
35 otp2289.OTPGenerator.tokens_to_bytes('INCH SEA ANNE LONG AHEM TOUR')
36 )
37 assert bytes.fromhex('7965e05436f5029f') == (
38 otp2289.OTPGenerator.tokens_to_bytes('EASE OIL FUM CURE AWRY AVIS')
39 )
40 assert bytes.fromhex('50fe1962c4965880') == (
41 otp2289.OTPGenerator.tokens_to_bytes('BAIL TUFT BITS GANG CHEF THY')
42 )
43 assert bytes.fromhex('87066dd9644bf206') == (
44 otp2289.OTPGenerator.tokens_to_bytes('FULL PEW DOWN ONCE MORT ARC')
45 )
46 assert bytes.fromhex('7cd34c1040add14b') == (
47 otp2289.OTPGenerator.tokens_to_bytes('FACT HOOF AT FIST SITE KENT')
48 )
49 assert bytes.fromhex('5aa37a81f212146c') == (
50 otp2289.OTPGenerator.tokens_to_bytes('BODE HOP JAKE STOW JUT RAP')
51 )
52 assert bytes.fromhex('f205753943de4cf9') == (
53 otp2289.OTPGenerator.tokens_to_bytes('ULAN NEW ARMY FUSE SUIT EYED')
54 )
55 assert bytes.fromhex('ddcdac956f234937') == (
56 otp2289.OTPGenerator.tokens_to_bytes('SKIM CULT LOB SLAM POE HOWL')
57 )
58 assert bytes.fromhex('b203e28fa525be47') == (
59 otp2289.OTPGenerator.tokens_to_bytes('LONG IVY JULY AJAR BOND LEE')
60 )
61 assert bytes.fromhex('bb9e6ae1979d8ff4') == (
62 otp2289.OTPGenerator.tokens_to_bytes('MILT VARY MAST OK SEES WENT')
63 )
64 assert bytes.fromhex('63d936639734385b') == (
65 otp2289.OTPGenerator.tokens_to_bytes('CART OTTO HIVE ODE VAT NUT')
66 )
67 assert bytes.fromhex('87fec7768b73ccf9') == (
68 otp2289.OTPGenerator.tokens_to_bytes('GAFF WAIT SKID GIG SKY EYED')
69 )
70 assert bytes.fromhex('ad85f658ebe383c9') == (
71 otp2289.OTPGenerator.tokens_to_bytes('LEST OR HEEL SCOT ROB SUIT')
72 )
73 assert bytes.fromhex('d07ce229b5cf119b') == (
74 otp2289.OTPGenerator.tokens_to_bytes('RITE TAKE GELD COST TUNE RECK')
75 )
76 assert bytes.fromhex('27bc71035aaf3dc6') == (
77 otp2289.OTPGenerator.tokens_to_bytes('MAY STAR TIN LYON VEDA STAN')
78 )
79 assert bytes.fromhex('d51f3e99bf8e6f0b') == (
80 otp2289.OTPGenerator.tokens_to_bytes('RUST WELT KICK FELL TAIL FRAU')
81 )
82 assert bytes.fromhex('82aeb52d943774e4') == (
83 otp2289.OTPGenerator.tokens_to_bytes('FLIT DOSE ALSO MEW DRUM DEFY')
84 )
85 assert bytes.fromhex('4f296a74fe1567ec') == (
86 otp2289.OTPGenerator.tokens_to_bytes('AURA ALOE HURL WING BERG WAIT')
87 )
88
89
90def test_random_bytes() -> None:
91 """Implement a few tests with random bytes"""
92 for _ in range(10):
93 rnd_bytes = os.urandom(8) # 64 bits
94 tokens = otp2289.OTPResponse.bytes_to_tokens(rnd_bytes)
95 assert rnd_bytes == otp2289.OTPGenerator.tokens_to_bytes(tokens)