Coverage for tests/test_derivepassphrase.py: 100.000%

93 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-14 11:39 +0200

1# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""Test passphrase generation via derivepassphrase.Vault.""" 

6 

7from __future__ import annotations 

8 

9import math 

10from typing import Any 

11 

12import derivepassphrase 

13import pytest 

14 

15Vault = derivepassphrase.Vault 

16 

17class TestVault: 

18 

19 phrase = b'She cells C shells bye the sea shoars' 

20 google_phrase = rb': 4TVH#5:aZl8LueOT\{' 

21 twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9" 

22 

23 @pytest.mark.parametrize(['service', 'expected'], [ 

24 (b'google', google_phrase), 

25 ('twitter', twitter_phrase), 

26 ]) 

27 def test_200_basic_configuration(self, service, expected): 

28 assert Vault(phrase=self.phrase).generate(service) == expected 

29 

30 def test_201_phrase_dependence(self): 

31 assert ( 

32 Vault(phrase=(self.phrase + b'X')).generate('google') == 

33 b'n+oIz6sL>K*lTEWYRO%7' 

34 ) 

35 

36 def test_202_reproducibility_and_bytes_service_name(self): 

37 assert ( 

38 Vault(phrase=self.phrase).generate(b'google') == 

39 Vault(phrase=self.phrase).generate('google') 

40 ) 

41 

42 def test_203_reproducibility_and_bytearray_service_name(self): 

43 assert ( 

44 Vault(phrase=self.phrase).generate(b'google') == 

45 Vault(phrase=self.phrase).generate(bytearray(b'google')) 

46 ) 

47 

48 def test_210_nonstandard_length(self): 

49 assert ( 

50 Vault(phrase=self.phrase, length=4).generate('google') 

51 == b'xDFu' 

52 ) 

53 

54 def test_211_repetition_limit(self): 

55 assert ( 

56 Vault(phrase=b'', length=24, symbol=0, number=0, 

57 repeat=1).generate('asd') == 

58 b'IVTDzACftqopUXqDHPkuCIhV' 

59 ) 

60 

61 def test_212_without_symbols(self): 

62 assert ( 

63 Vault(phrase=self.phrase, symbol=0).generate('google') == 

64 b'XZ4wRe0bZCazbljCaMqR' 

65 ) 

66 

67 def test_213_no_numbers(self): 

68 assert ( 

69 Vault(phrase=self.phrase, number=0).generate('google') == 

70 b'_*$TVH.%^aZl(LUeOT?>' 

71 ) 

72 

73 def test_214_no_lowercase_letters(self): 

74 assert ( 

75 Vault(phrase=self.phrase, lower=0).generate('google') == 

76 b':{?)+7~@OA:L]!0E$)(+' 

77 ) 

78 

79 def test_215_at_least_5_digits(self): 

80 assert ( 

81 Vault(phrase=self.phrase, length=8, number=5) 

82 .generate('songkick') == b'i0908.7[' 

83 ) 

84 

85 def test_216_lots_of_spaces(self): 

86 assert ( 

87 Vault(phrase=self.phrase, space=12) 

88 .generate('songkick') == b' c 6 Bq % 5fR ' 

89 ) 

90 

91 def test_217_all_character_classes(self): 

92 assert ( 

93 Vault(phrase=self.phrase, lower=2, upper=2, number=1, 

94 space=3, dash=2, symbol=1) 

95 .generate('google') == b': : fv_wqt>a-4w1S R' 

96 ) 

97 

98 def test_218_only_numbers_and_very_high_repetition_limit(self): 

99 generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0, 

100 dash=0, symbol=0, repeat=4).generate('abcdef') 

101 forbidden_substrings = {b'0000', b'1111', b'2222', b'3333', b'4444', 

102 b'5555', b'6666', b'7777', b'8888', b'9999'} 

103 for substring in forbidden_substrings: 

104 assert substring not in generated 

105 

106 def test_219_very_limited_character_set(self): 

107 generated = Vault(phrase=b'', length=24, lower=0, upper=0, 

108 space=0, symbol=0).generate('testing') 

109 assert b'763252593304946694588866' == generated 

110 

111 def test_220_character_set_subtraction(self): 

112 assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') 

113 

114 @pytest.mark.parametrize(['length', 'settings', 'entropy'], [ 

115 (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)), 

116 ( 

117 20, 

118 {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0}, 

119 math.log2(math.factorial(20)) + 20 * math.log2(26) 

120 ), 

121 (0, {}, float('-inf')), 

122 (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')), 

123 (1, {}, math.log2(94)), 

124 (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0), 

125 ]) 

126 def test_221_entropy( 

127 self, length: int, settings: dict[str, int], entropy: int 

128 ) -> None: 

129 v = Vault(length=length, **settings) # type: ignore[arg-type] 

130 assert math.isclose(v._entropy(), entropy) 

131 assert v._estimate_sufficient_hash_length() > 0 

132 if math.isfinite(entropy) and entropy: 

133 assert ( 

134 v._estimate_sufficient_hash_length(1.0) == 

135 math.ceil(entropy / 8) 

136 ) 

137 assert v._estimate_sufficient_hash_length(8.0) >= entropy 

138 

139 def test_222_hash_length_estimation(self) -> None: 

140 v = Vault(phrase=self.phrase) 

141 v2 = Vault(phrase=self.phrase, lower=0, upper=0, number=0, 

142 symbol=0, space=1, length=1) 

143 assert v2._entropy() == 0.0 

144 assert v2._estimate_sufficient_hash_length() > 0 

145 

146 @pytest.mark.parametrize(['service', 'expected'], [ 

147 (b'google', google_phrase), 

148 ('twitter', twitter_phrase), 

149 ]) 

150 def test_223_hash_length_expansion( 

151 self, monkeypatch: Any, service: str | bytes, expected: bytes 

152 ) -> None: 

153 v = Vault(phrase=self.phrase) 

154 monkeypatch.setattr(v, 

155 '_estimate_sufficient_hash_length', 

156 lambda *args, **kwargs: 1) 

157 assert v._estimate_sufficient_hash_length() < len(self.phrase) 

158 assert v.generate(service) == expected 

159 

160 @pytest.mark.parametrize(['s', 'raises'], [ 

161 ('ñ', True), ('Düsseldorf', True), 

162 ('liberté, egalité, fraternité', True), ('ASCII', False), 

163 ('Düsseldorf'.encode('UTF-8'), False), 

164 (bytearray([2, 3, 5, 7, 11, 13]), False), 

165 ]) 

166 def test_224_binary_strings( 

167 self, s: str | bytes | bytearray, raises: bool 

168 ) -> None: 

169 binstr = derivepassphrase.Vault._get_binary_string 

170 AmbiguousByteRepresentationError = ( 

171 derivepassphrase.AmbiguousByteRepresentationError 

172 ) 

173 if raises: 

174 with pytest.raises(AmbiguousByteRepresentationError): 

175 binstr(s) 

176 elif isinstance(s, str): 

177 assert binstr(s) == s.encode('UTF-8') 

178 assert binstr(binstr(s)) == s.encode('UTF-8') 

179 else: 

180 assert binstr(s) == bytes(s) 

181 assert binstr(binstr(s)) == bytes(s) 

182 

183 def test_310_too_many_symbols(self): 

184 with pytest.raises(ValueError, 

185 match='requested passphrase length too short'): 

186 Vault(phrase=self.phrase, symbol=100) 

187 

188 def test_311_no_viable_characters(self): 

189 with pytest.raises(ValueError, 

190 match='no allowed characters left'): 

191 Vault(phrase=self.phrase, lower=0, upper=0, number=0, 

192 space=0, dash=0, symbol=0) 

193 

194 def test_320_character_set_subtraction_duplicate(self): 

195 with pytest.raises(ValueError, match='duplicate characters'): 

196 Vault._subtract(b'abcdef', b'aabbccddeeff') 

197 with pytest.raises(ValueError, match='duplicate characters'): 

198 Vault._subtract(b'aabbccddeeff', b'abcdef') 

199 

200 def test_322_hash_length_estimation(self) -> None: 

201 v = Vault(phrase=self.phrase) 

202 with pytest.raises(ValueError, 

203 match='invalid safety factor'): 

204 assert v._estimate_sufficient_hash_length(-1.0) 

205 with pytest.raises(TypeError, 

206 match='invalid safety factor: not a float'): 

207 assert v._estimate_sufficient_hash_length(None) # type: ignore