|
1 | | -"""Tests for `_lib.e2ee` store-error diagnosis. |
| 1 | +"""Tests for `_lib.e2ee`: store-error diagnosis and scoped credential deletion. |
2 | 2 |
|
3 | 3 | The skill directory contains a hyphen (`matrix-communication`) so it is not |
4 | 4 | importable as a package; run the file directly or use unittest discovery: |
|
16 | 16 | package and breaks `urllib` on the way in. |
17 | 17 | """ |
18 | 18 |
|
| 19 | +import json |
19 | 20 | import os |
| 21 | +import pathlib |
| 22 | +import shutil |
20 | 23 | import sys |
| 24 | +import tempfile |
21 | 25 | import unittest |
22 | 26 |
|
23 | 27 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
24 | 28 |
|
25 | | -from e2ee import explain_store_error, restore_login_checked |
| 29 | +import e2ee |
| 30 | +from e2ee import ( |
| 31 | + delete_credentials, |
| 32 | + explain_store_error, |
| 33 | + restore_login_checked, |
| 34 | + store_files_for, |
| 35 | +) |
26 | 36 |
|
27 | 37 |
|
28 | 38 | class OlmAccountError(Exception): |
@@ -86,5 +96,78 @@ def test_unrelated_error_is_reraised_untouched(self): |
86 | 96 | restore_login_checked(client, "@u:example.org", "DEVICE", "syt_token") |
87 | 97 |
|
88 | 98 |
|
| 99 | +class StoreScopingTests(unittest.TestCase): |
| 100 | + """--logout must take one device's files and leave every other device alone. |
| 101 | +
|
| 102 | + Regression for #81: the old code globbed `*.db` and `*_devices` across the |
| 103 | + shared store directory, so logging one device out destroyed the megolm |
| 104 | + history of all of them. |
| 105 | + """ |
| 106 | + |
| 107 | + USER = "@user:example.org" |
| 108 | + MINE = "DEVICEAAAA" |
| 109 | + OTHER = "DEVICEBBBB" |
| 110 | + |
| 111 | + def setUp(self): |
| 112 | + self.store = pathlib.Path(tempfile.mkdtemp()) |
| 113 | + self.addCleanup(shutil.rmtree, self.store, True) |
| 114 | + real = e2ee.get_store_path |
| 115 | + e2ee.get_store_path = lambda: self.store |
| 116 | + self.addCleanup(setattr, e2ee, "get_store_path", real) |
| 117 | + |
| 118 | + for device in (self.MINE, self.OTHER): |
| 119 | + for suffix in ( |
| 120 | + "db", |
| 121 | + "blacklisted_devices", |
| 122 | + "ignored_devices", |
| 123 | + "trusted_devices", |
| 124 | + ): |
| 125 | + (self.store / f"{self.USER}_{device}.{suffix}").write_text("x") |
| 126 | + |
| 127 | + # Not device-scoped, and the key import depends on it. |
| 128 | + (self.store / "backup_key.json").write_text("{}") |
| 129 | + (self.store / "credentials.json").write_text( |
| 130 | + json.dumps({"user_id": self.USER, "device_id": self.MINE}) |
| 131 | + ) |
| 132 | + |
| 133 | + def _names(self): |
| 134 | + return sorted(p.name for p in self.store.iterdir()) |
| 135 | + |
| 136 | + def test_store_files_for_selects_one_device(self): |
| 137 | + names = sorted(p.name for p in store_files_for(self.USER, self.MINE)) |
| 138 | + self.assertEqual(len(names), 4) |
| 139 | + self.assertTrue(all(self.MINE in n for n in names)) |
| 140 | + |
| 141 | + def test_store_files_for_does_not_match_a_prefix_device_id(self): |
| 142 | + """A device id that is a prefix of another must not collect its files.""" |
| 143 | + (self.store / f"{self.USER}_{self.MINE}EXTRA.db").write_text("x") |
| 144 | + names = [p.name for p in store_files_for(self.USER, self.MINE)] |
| 145 | + self.assertNotIn(f"{self.USER}_{self.MINE}EXTRA.db", names) |
| 146 | + |
| 147 | + def test_logout_removes_only_this_device(self): |
| 148 | + removed = delete_credentials() |
| 149 | + |
| 150 | + self.assertIn("credentials.json", removed) |
| 151 | + self.assertEqual(len([n for n in removed if self.MINE in n]), 4) |
| 152 | + |
| 153 | + left = self._names() |
| 154 | + self.assertEqual(len([n for n in left if self.OTHER in n]), 4) |
| 155 | + self.assertIn("backup_key.json", left) |
| 156 | + self.assertNotIn("credentials.json", left) |
| 157 | + |
| 158 | + def test_purge_all_removes_every_device(self): |
| 159 | + delete_credentials(purge_all=True) |
| 160 | + left = self._names() |
| 161 | + self.assertEqual([n for n in left if n.endswith("_devices")], []) |
| 162 | + self.assertEqual([n for n in left if n.endswith(".db")], []) |
| 163 | + self.assertIn("backup_key.json", left) |
| 164 | + |
| 165 | + def test_without_credentials_nothing_is_removed(self): |
| 166 | + """No credentials means no device to scope by - deleting nothing is right.""" |
| 167 | + (self.store / "credentials.json").unlink() |
| 168 | + self.assertEqual(delete_credentials(), []) |
| 169 | + self.assertEqual(len(self._names()), 9) |
| 170 | + |
| 171 | + |
89 | 172 | if __name__ == "__main__": |
90 | 173 | unittest.main(verbosity=2) |
0 commit comments