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