Coverage for tests/test_derivepassphrase_vault.py: 100.000%
99 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-06-23 21:50 +0200
« prev ^ index » next coverage.py v7.5.1, created at 2024-06-23 21:50 +0200
1# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2#
3# SPDX-License-Identifier: MIT
5"""Test passphrase generation via derivepassphrase.Vault."""
7from __future__ import annotations
9import math
10from typing import Any
12import derivepassphrase
13import sequin
14import pytest
15import tests
17Vault = derivepassphrase.Vault
18phrase = b'She cells C shells bye the sea shoars'
19google_phrase = rb': 4TVH#5:aZl8LueOT\{'
20twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9"
22@pytest.mark.parametrize(['service', 'expected'], [
23 (b'google', google_phrase),
24 ('twitter', twitter_phrase),
25])
26def test_200_basic_configuration(service, expected):
27 assert Vault(phrase=phrase).generate(service) == expected
29def test_201_phrase_dependence():
30 assert (
31 Vault(phrase=(phrase + b'X')).generate('google') ==
32 b'n+oIz6sL>K*lTEWYRO%7'
33 )
35def test_202_reproducibility_and_bytes_service_name():
36 assert (
37 Vault(phrase=phrase).generate(b'google') ==
38 Vault(phrase=phrase).generate('google')
39 )
41def test_203_reproducibility_and_bytearray_service_name():
42 assert (
43 Vault(phrase=phrase).generate(b'google') ==
44 Vault(phrase=phrase).generate(bytearray(b'google'))
45 )
47def test_210_nonstandard_length():
48 assert Vault(phrase=phrase, length=4).generate('google') == b'xDFu'
50def test_211_repetition_limit():
51 assert (
52 Vault(phrase=b'', length=24, symbol=0, number=0,
53 repeat=1).generate('asd') ==
54 b'IVTDzACftqopUXqDHPkuCIhV'
55 )
57def test_212_without_symbols():
58 assert (
59 Vault(phrase=phrase, symbol=0).generate('google') ==
60 b'XZ4wRe0bZCazbljCaMqR'
61 )
63def test_213_too_many_symbols():
64 with pytest.raises(ValueError,
65 match='requested passphrase length too short'):
66 Vault(phrase=phrase, symbol=100)
68def test_214_no_numbers():
69 assert (
70 Vault(phrase=phrase, number=0).generate('google') ==
71 b'_*$TVH.%^aZl(LUeOT?>'
72 )
74def test_214_no_lowercase_letters():
75 assert (
76 Vault(phrase=phrase, lower=0).generate('google') ==
77 b':{?)+7~@OA:L]!0E$)(+'
78 )
80def test_215_at_least_5_digits():
81 assert (
82 Vault(phrase=phrase, length=8, number=5).generate('songkick') ==
83 b'i0908.7['
84 )
86def test_216_lots_of_spaces():
87 assert (
88 Vault(phrase=phrase, space=12).generate('songkick') ==
89 b' c 6 Bq % 5fR '
90 )
92def test_217_no_viable_characters():
93 with pytest.raises(ValueError,
94 match='no allowed characters left'):
95 Vault(phrase=phrase, lower=0, upper=0, number=0,
96 space=0, dash=0, symbol=0)
98def test_218_all_character_classes():
99 assert (
100 Vault(phrase=phrase, lower=2, upper=2, number=1,
101 space=3, dash=2, symbol=1).generate('google') ==
102 b': : fv_wqt>a-4w1S R'
103 )
105def test_219_only_numbers_and_very_high_repetition_limit():
106 generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0,
107 dash=0, symbol=0, repeat=4).generate('abcdef')
108 assert b'0000' not in generated
109 assert b'1111' not in generated
110 assert b'2222' not in generated
111 assert b'3333' not in generated
112 assert b'4444' not in generated
113 assert b'5555' not in generated
114 assert b'6666' not in generated
115 assert b'7777' not in generated
116 assert b'8888' not in generated
117 assert b'9999' not in generated
119def test_220_very_limited_character_set():
120 generated = Vault(phrase=b'', length=24, lower=0, upper=0,
121 space=0, symbol=0).generate('testing')
122 assert b'763252593304946694588866' == generated
124def test_300_character_set_subtraction():
125 assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf')
127def test_301_character_set_subtraction_duplicate():
128 with pytest.raises(ValueError, match='duplicate characters'):
129 Vault._subtract(b'abcdef', b'aabbccddeeff')
130 with pytest.raises(ValueError, match='duplicate characters'):
131 Vault._subtract(b'aabbccddeeff', b'abcdef')
133@pytest.mark.parametrize(['length', 'settings', 'entropy'], [
134 (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)),
135 (
136 20,
137 {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0},
138 math.log2(math.factorial(20)) + 20 * math.log2(26)
139 ),
140 (0, {}, float('-inf')),
141 (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')),
142 (1, {}, math.log2(94)),
143 (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0),
144])
145def test_400_entropy(
146 length: int, settings: dict[str, int], entropy: int
147) -> None:
148 v = Vault(length=length, **settings) # type: ignore[arg-type]
149 assert math.isclose(v._entropy(), entropy)
150 assert v._estimate_sufficient_hash_length() > 0
151 if math.isfinite(entropy) and entropy:
152 assert v._estimate_sufficient_hash_length(1.0) == math.ceil(entropy / 8)
153 assert v._estimate_sufficient_hash_length(8.0) >= entropy
155def test_401_hash_length_estimation(
156) -> None:
157 v = Vault(phrase=phrase)
158 with pytest.raises(ValueError,
159 match='invalid safety factor'):
160 assert v._estimate_sufficient_hash_length(-1.0)
161 with pytest.raises(TypeError,
162 match='invalid safety factor: not a float'):
163 assert v._estimate_sufficient_hash_length(None) # type: ignore
164 v2 = Vault(phrase=phrase, lower=0, upper=0, number=0, symbol=0,
165 space=1, length=1)
166 assert v2._entropy() == 0.0
167 assert v2._estimate_sufficient_hash_length() > 0
169@pytest.mark.parametrize(['service', 'expected'], [
170 (b'google', google_phrase),
171 ('twitter', twitter_phrase),
172])
173def test_402_hash_length_expansion(
174 monkeypatch: Any, service: str | bytes, expected: bytes
175) -> None:
176 v = Vault(phrase=phrase)
177 monkeypatch.setattr(v,
178 '_estimate_sufficient_hash_length',
179 lambda *args, **kwargs: 1)
180 assert v._estimate_sufficient_hash_length() < len(phrase)
181 assert v.generate(service) == expected
183@pytest.mark.parametrize(['s', 'raises'], [
184 ('ñ', True), ('Düsseldorf', True),
185 ('liberté, egalité, fraternité', True), ('ASCII', False),
186 ('Düsseldorf'.encode('UTF-8'), False),
187 (bytearray([2, 3, 5, 7, 11, 13]), False),
188])
189def test_403_binary_strings(s: str | bytes | bytearray, raises: bool) -> None:
190 binstr = derivepassphrase.Vault._get_binary_string
191 if raises:
192 with pytest.raises(derivepassphrase.AmbiguousByteRepresentationError):
193 binstr(s)
194 elif isinstance(s, str):
195 assert binstr(s) == s.encode('UTF-8')
196 assert binstr(binstr(s)) == s.encode('UTF-8')
197 else:
198 assert binstr(s) == bytes(s)
199 assert binstr(binstr(s)) == bytes(s)