Coverage for tests / test_derivepassphrase_ssh_agent / test_heavy_duty.py: 100.000%

78 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 21:34 +0200

1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> 

2# 

3# SPDX-License-Identifier: Zlib 

4 

5"""Tests for `derivepassphrase.ssh_agent`: heavy-duty tests.""" 

6 

7from __future__ import annotations 

8 

9import contextlib 

10 

11import pytest 

12from hypothesis import stateful, strategies 

13 

14from derivepassphrase import _types 

15from derivepassphrase.ssh_agent import socketprovider 

16from tests.machinery import pytest as pytest_machinery 

17 

18# All tests in this module are heavy-duty tests. 

19pytestmark = [pytest_machinery.heavy_duty] 

20 

21 

22@strategies.composite 

23def draw_alias_chain( 

24 draw: strategies.DrawFn, 

25 *, 

26 known_keys_strategy: strategies.SearchStrategy[str], 

27 new_keys_strategy: strategies.SearchStrategy[str], 

28 chain_size: strategies.SearchStrategy[int] = strategies.integers( # noqa: B008 

29 min_value=1, 

30 max_value=5, 

31 ), 

32 existing: bool = False, 

33) -> tuple[str, ...]: 

34 """Draw names for alias chains in the SSH agent socket provider registry. 

35 

36 Depending on arguments, draw a set of names from the new keys bundle 

37 that do not yet exist in the registry, to insert as a new alias 

38 chain. Alternatively, draw a non-alias name from the known keys 

39 bundle, then draw other names that either don't exist yet in the 

40 registry, or that alias the first name directly or indirectly. The 

41 chain length, and whether to target existing registry entries or 

42 not, may be set statically, or may be drawn from a respective 

43 strategy. 

44 

45 Args: 

46 draw: 

47 The `hypothesis` draw function. 

48 chain_size: 

49 A strategy for determining the correct alias chain length. 

50 Must not yield any integers less than 1. 

51 existing: 

52 If true, target an existing registry entry in the alias 

53 chain, and permit rewriting existing aliases of that same 

54 entry to the new alias. Otherwise, draw only new names. 

55 known_keys_strategy: 

56 A strategy for generating provider registry keys already 

57 contained in the registry. Typically, this is 

58 a [Bundle][hypothesis.stateful.Bundle]. 

59 new_keys_strategy: 

60 A strategy for generating provider registry keys not yet 

61 contained in the registry with high probability. Typically, 

62 this is a [consuming][hypothesis.stateful.consumes] 

63 [Bundle][hypothesis.stateful.Bundle]. 

64 

65 Returns: 

66 A tuple of names forming an alias chain, each entry pointing to 

67 or intending to point to the previous entry in the tuple. 

68 

69 """ 

70 registry = socketprovider.SocketProvider.registry 1a

71 

72 def not_an_alias(key: str) -> bool: 1a

73 return key in registry and not isinstance(registry[key], str) 1a

74 

75 def is_indirect_alias_of( 1a

76 key: str, target: str 1a

77 ) -> bool: # pragma: no cover 1a

78 if key == target: 1a

79 return False # not an alias 1a

80 seen = set() # loop detection 1a

81 while key not in seen: 1a

82 seen.add(key) 1a

83 if key not in registry: 1a

84 return False 

85 if not isinstance(registry[key], str): 1a

86 return False 1a

87 if key == target: 1a

88 return True 

89 tmp = registry[key] 1a

90 assert isinstance(tmp, str) 1a

91 key = tmp 1a

92 return False # loop 

93 

94 err_msg_chain_size = "Chain sizes must always be 1 or larger." 1a

95 

96 size = draw(chain_size, label="chain_size") 1a

97 if size < 1: # pragma: no cover 1a

98 raise ValueError(err_msg_chain_size) 

99 names: list[str] = [] 1a

100 base: str | None = None 1a

101 if existing: 1a

102 names.append( 1a

103 draw(known_keys_strategy.filter(not_an_alias), label="base") 

104 ) 

105 base = names[0] 1a

106 size -= 1 1a

107 new_key_strategy = new_keys_strategy.filter( 1a

108 lambda key: key not in registry 

109 ) 

110 old_key_strategy = known_keys_strategy.filter( 1a

111 lambda key: is_indirect_alias_of(key, target=base) 

112 ) 

113 list_strategy_source = strategies.one_of( 1a

114 new_key_strategy, old_key_strategy 

115 ) 

116 else: 

117 list_strategy_source = new_keys_strategy.filter( 1a

118 lambda key: key not in registry 

119 ) 

120 list_strategy = strategies.lists( 1a

121 list_strategy_source.filter(lambda candidate: candidate != base), 

122 min_size=size, 

123 max_size=size, 

124 unique=True, 

125 ) 

126 names.extend(draw(list_strategy, label="others")) 1a

127 return tuple(names) 1a

128 

129 

130class SSHAgentSocketProviderRegistryStateMachine( 

131 stateful.RuleBasedStateMachine 

132): 

133 """A state machine for the SSH agent socket provider registry. 

134 

135 Record possible changes to the socket provider registry, keeping track 

136 of true entries, aliases, and reservations. 

137 

138 """ 

139 

140 def __init__(self) -> None: 1ba

141 """Initialize self, set up context managers and enter them.""" 

142 super().__init__() 1a

143 self.exit_stack = contextlib.ExitStack().__enter__() 1a

144 self.monkeypatch = self.exit_stack.enter_context( 1a

145 pytest.MonkeyPatch.context() 

146 ) 

147 self.orig_registry = socketprovider.SocketProvider.registry 1a

148 self.registry: dict[ 1a

149 str, _types.SSHAgentSocketProvider | str | None 

150 ] = { 

151 _types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_POSIX: ( 

152 self.orig_registry[ 

153 _types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_POSIX 

154 ] 

155 ), 

156 _types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_WINDOWS: ( 

157 self.orig_registry[ 

158 _types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_WINDOWS 

159 ] 

160 ), 

161 _types.BuiltinSSHAgentSocketProvider.NATIVE: ( 

162 _types.BuiltinSSHAgentSocketProvider.WINDOWS_NAMED_PIPE 

163 if self.orig_registry[ 

164 _types.BuiltinSSHAgentSocketProvider.NATIVE 

165 ] 

166 == _types.BuiltinSSHAgentSocketProvider.WINDOWS 

167 else _types.BuiltinSSHAgentSocketProvider.UNIX_DOMAIN 

168 ), 

169 _types.BuiltinSSHAgentSocketProvider.UNIX_DOMAIN: ( 

170 _types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_POSIX 

171 ), 

172 _types.BuiltinSSHAgentSocketProvider.WINDOWS_NAMED_PIPE: ( 

173 _types.BuiltinSSHAgentSocketProvider.SSH_AUTH_SOCK_ON_WINDOWS 

174 ), 

175 } 

176 self.monkeypatch.setattr( 1a

177 socketprovider.SocketProvider, "registry", self.registry 

178 ) 

179 self.model: dict[str, _types.SSHAgentSocketProvider | None] = {} 1a

180 

181 known_keys: stateful.Bundle[str] = stateful.Bundle("known_keys") 

182 """""" 

183 new_keys: stateful.Bundle[str] = stateful.Bundle("new_keys") 

184 """""" 

185 

186 def sample_provider(self) -> _types.SSHAgentSocket: 

187 raise AssertionError 

188 

189 @stateful.initialize( 

190 target=known_keys, 

191 ) 

192 def get_registry_keys(self) -> stateful.MultipleResults[str]: 

193 """Read the standard keys from the registry.""" 

194 self.model.update({ 1a

195 k: socketprovider.SocketProvider.lookup(k) for k in self.registry 

196 }) 

197 return stateful.multiple(*self.registry.keys()) 1a

198 

199 @stateful.rule( 

200 target=new_keys, 

201 k=strategies.text("abcdefghijklmnopqrstuvwxyz0123456789_").filter( 

202 lambda s: s not in socketprovider.SocketProvider.registry 

203 ), 

204 ) 

205 def new_key(self, k: str) -> str: 

206 return k 1a

207 

208 @stateful.invariant() 

209 def check_consistency(self) -> None: 

210 lookup = socketprovider.SocketProvider.lookup 1a

211 assert self.registry.keys() == self.model.keys() 1a

212 for k in self.model: 1a

213 resolved = lookup(k) 1a

214 modelled = self.model[k] 1a

215 step1 = self.registry[k] 1a

216 manually = lookup(step1) if isinstance(step1, str) else step1 1a

217 assert resolved == modelled 1a

218 assert resolved == manually 1a

219 

220 @stateful.rule( 

221 target=known_keys, 

222 chain=draw_alias_chain( 

223 known_keys_strategy=known_keys, 

224 new_keys_strategy=stateful.consumes(new_keys), 

225 existing=True, 

226 ), 

227 ) 

228 def alias_existing( 

229 self, chain: tuple[str, ...] 

230 ) -> stateful.MultipleResults[str]: 

231 try: 1a

232 provider = socketprovider.SocketProvider.resolve(chain[0]) 1a

233 except NotImplementedError: # pragma: no cover [failsafe] 

234 provider = self.sample_provider 

235 assert ( 1a

236 socketprovider.SocketProvider.register(*chain)(provider) 

237 == provider 

238 ) 

239 for k in chain: 1a

240 self.model[k] = provider 1a

241 return stateful.multiple(*chain[1:]) 1a

242 

243 @stateful.rule( 

244 target=known_keys, 

245 chain=draw_alias_chain( 

246 known_keys_strategy=known_keys, 

247 new_keys_strategy=stateful.consumes(new_keys), 

248 existing=False, 

249 ), 

250 ) 

251 def alias_new(self, chain: list[str]) -> stateful.MultipleResults[str]: 

252 provider = self.sample_provider 1a

253 assert ( 1a

254 socketprovider.SocketProvider.register(*chain)(provider) 

255 == provider 

256 ) 

257 for k in chain: 1a

258 self.model[k] = provider 1a

259 return stateful.multiple(*chain) 1a

260 

261 def teardown(self) -> None: 

262 """Upon teardown, exit all contexts entered in `__init__`.""" 

263 self.exit_stack.close() 1a

264 

265 

266TestSSHAgentSocketProviderRegistry = ( 

267 SSHAgentSocketProviderRegistryStateMachine.TestCase 

268)