Coverage for tests/ledger/test_pin.py: 100%

90 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-07-10 13:43 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2021 RSK Labs Ltd 

4# 

5# Permission is hereby granted, free of charge, to any person obtaining a copy of 

6# this software and associated documentation files (the "Software"), to deal in 

7# the Software without restriction, including without limitation the rights to 

8# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 

9# of the Software, and to permit persons to whom the Software is furnished to do 

10# so, subject to the following conditions: 

11# 

12# The above copyright notice and this permission notice shall be included in all 

13# copies or substantial portions of the Software. 

14# 

15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 

21# SOFTWARE. 

22 

23import string 

24from unittest import TestCase 

25from unittest.mock import MagicMock, call, patch 

26from parameterized import parameterized 

27import ledger.pin as pin 

28 

29 

30class TestBasePin(TestCase): 

31 def test_generate_pin(self): 

32 for i in range(1000): 

33 p = pin.BasePin.generate_pin() 

34 self.assertEqual(8, len(p)) 

35 self.assertEqual(bytes, type(p)) 

36 self.assertTrue( 

37 all(map(lambda c: chr(c) in string.ascii_letters + string.digits, p))) 

38 self.assertTrue(any(map(lambda c: chr(c) in string.ascii_letters, p))) 

39 

40 @parameterized.expand([ 

41 ("abc", False), 

42 ("abcd", False), 

43 ("abc1", False), 

44 ("abcde", False), 

45 ("abc12", False), 

46 ("abcdef", False), 

47 ("abc12f", False), 

48 ("abcdefg", False), 

49 ("ab2d68g", False), 

50 ("abcdefgh", True), 

51 ("8b23ef1s", True), 

52 ("abcdefghi", False), 

53 ("MNO", False), 

54 ("MNOP", False), 

55 ("MNO2", False), 

56 ("MNOPQ", False), 

57 ("MN3P4", False), 

58 ("MNOPQR", False), 

59 ("M1245R", False), 

60 ("MNOPQRS", False), 

61 ("M656QR3", False), 

62 ("MNOPQRST", True), 

63 ("MN22P3S9", True), 

64 ("MNOPQRSTU", False), 

65 ("1NO4Q6S8U", False), 

66 ("1234", False), 

67 ("123456", False), 

68 ("12345678", False), 

69 ("some-th", False), 

70 ("a1-@.;", False), 

71 ("!@#$%^&*", False), 

72 ("(),./;']", False), 

73 ]) 

74 def test_is_valid(self, p, expected_validity): 

75 self.assertEqual(pin.BasePin.is_valid(p.encode()), expected_validity) 

76 

77 @parameterized.expand([ 

78 ("abc", True), 

79 ("abcd", True), 

80 ("abc1", True), 

81 ("abcde", True), 

82 ("abc12", True), 

83 ("abcdef", True), 

84 ("abc12f", True), 

85 ("abcdefg", True), 

86 ("ab2d68g", True), 

87 ("abcdefgh", True), 

88 ("8b23ef1s", True), 

89 ("abcdefghi", True), 

90 ("MNO", True), 

91 ("MNOP", True), 

92 ("MNO2", True), 

93 ("MNOPQ", True), 

94 ("MN3P4", True), 

95 ("MNOPQR", True), 

96 ("M1245R", True), 

97 ("MNOPQRS", True), 

98 ("M656QR3", True), 

99 ("MNOPQRST", True), 

100 ("MN22P3S9", True), 

101 ("MNOPQRSTU", True), 

102 ("1NO4Q6S8U", True), 

103 ("1234", True), 

104 ("123456", True), 

105 ("12345678", True), 

106 ("some-th", False), 

107 ("a1-@.;", False), 

108 ("!@#$%^&*", False), 

109 ("(),./;']", False), 

110 ]) 

111 def test_is_valid_any_pin(self, p, expected_validity): 

112 self.assertEqual(pin.BasePin.is_valid(p.encode(), any_pin=True), 

113 expected_validity) 

114 

115 

116class TestFileBasedPin(TestCase): 

117 @patch("ledger.pin.open") 

118 def test_new(self, mock_open): 

119 mock_file = MagicMock() 

120 mock_open.return_value = mock_file 

121 p = pin.FileBasedPin.new("a-path") 

122 self.assertTrue(pin.BasePin.is_valid(p)) 

123 self.assertEqual([call("a-path", "wb")], mock_open.call_args_list) 

124 self.assertEqual([call(p)], mock_file.__enter__().write.call_args_list) 

125 self.assertTrue(mock_file.__exit__.called) 

126 

127 @patch("os.path.isfile") 

128 @patch("ledger.pin.open") 

129 def test_init_pin_doesnotexist(self, mock_open, mock_isfile): 

130 mock_isfile.return_value = False 

131 p = pin.FileBasedPin("a-path", b"abcdefgh") 

132 self.assertEqual([call("a-path")], mock_isfile.call_args_list) 

133 self.assertFalse(mock_open.called) 

134 self.assertEqual(b"abcdefgh", p.get_pin()) 

135 self.assertTrue(p.needs_change()) 

136 

137 @patch("os.path.isfile") 

138 @patch("ledger.pin.open") 

139 def test_init_pin_exists(self, mock_open, mock_isfile): 

140 mock_isfile.return_value = True 

141 mock_file = MagicMock() 

142 mock_file.__enter__().read.return_value = b"othpinab\n" 

143 mock_open.return_value = mock_file 

144 p = pin.FileBasedPin("a-path", b"abcdefgh") 

145 self.assertEqual([call("a-path")], mock_isfile.call_args_list) 

146 self.assertEqual([call("a-path", "rb")], mock_open.call_args_list) 

147 self.assertEqual(b"othpinab", p.get_pin()) 

148 self.assertFalse(p.needs_change()) 

149 

150 @patch("os.path.isfile") 

151 @patch("ledger.pin.open") 

152 def test_pin_change(self, mock_open, mock_isfile): 

153 mock_isfile.return_value = False 

154 mock_file = MagicMock() 

155 mock_open.return_value = mock_file 

156 p = pin.FileBasedPin("a-path", b"abcdefgh") 

157 self.assertEqual(b"abcdefgh", p.get_pin()) 

158 self.assertEqual(None, p.get_new_pin()) 

159 self.assertTrue(p.needs_change()) 

160 p.start_change() 

161 np = p.get_new_pin() 

162 self.assertEqual(b"abcdefgh", p.get_pin()) 

163 self.assertNotEqual(None, np) 

164 self.assertTrue(pin.BasePin.is_valid(np)) 

165 p.commit_change() 

166 self.assertEqual(np, p.get_pin()) 

167 self.assertEqual(None, p.get_new_pin()) 

168 self.assertFalse(p.needs_change()) 

169 self.assertEqual([call("a-path", "wb")], mock_open.call_args_list) 

170 self.assertEqual([call(np)], mock_file.__enter__().write.call_args_list) 

171 self.assertTrue(mock_file.__exit__.called) 

172 

173 @patch("os.path.isfile") 

174 @patch("ledger.pin.open") 

175 def test_pin_change_aborted(self, mock_open, mock_isfile): 

176 mock_isfile.return_value = False 

177 p = pin.FileBasedPin("a-path", b"abcdefgh") 

178 self.assertEqual(b"abcdefgh", p.get_pin()) 

179 self.assertEqual(None, p.get_new_pin()) 

180 self.assertTrue(p.needs_change()) 

181 p.start_change() 

182 np = p.get_new_pin() 

183 self.assertEqual(b"abcdefgh", p.get_pin()) 

184 self.assertNotEqual(None, np) 

185 self.assertTrue(pin.BasePin.is_valid(np)) 

186 p.abort_change() 

187 self.assertEqual(b"abcdefgh", p.get_pin()) 

188 self.assertEqual(None, p.get_new_pin()) 

189 self.assertTrue(p.needs_change()) 

190 self.assertFalse(mock_open.called)