fix(client): fail closed on polling configuration drift
This commit is contained in:
@@ -46,6 +46,15 @@ class FakeProtector:
|
||||
return plaintext
|
||||
|
||||
|
||||
class NoUnprotectProtector(FakeProtector):
|
||||
def __init__(self) -> None:
|
||||
self.unprotect_calls = 0
|
||||
|
||||
def unprotect(self, ciphertext: bytes, *, purpose: str) -> bytes:
|
||||
self.unprotect_calls += 1
|
||||
raise AssertionError("metadata_read_must_not_unprotect")
|
||||
|
||||
|
||||
def settings(device_id: str = DEVICE_ID) -> ProfileSettings:
|
||||
return ProfileSettings(
|
||||
PROFILE,
|
||||
@@ -113,19 +122,95 @@ class LocalStateStoreTests(unittest.TestCase):
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_profile_summary_reads_settings_and_token_presence_without_unprotect(self) -> None:
|
||||
class NoUnprotectProtector(FakeProtector):
|
||||
def unprotect(self, ciphertext: bytes, *, purpose: str) -> bytes:
|
||||
raise AssertionError("metadata_read_must_not_unprotect")
|
||||
|
||||
protector = NoUnprotectProtector()
|
||||
summary_store = LocalStateStore(
|
||||
self.database,
|
||||
NoUnprotectProtector(),
|
||||
protector,
|
||||
now=lambda: self.clock[0],
|
||||
)
|
||||
summary = summary_store.load_profile_summary(PROFILE)
|
||||
self.assertEqual(summary.settings, settings())
|
||||
self.assertTrue(summary.has_stored_device_token)
|
||||
self.assertNotIn(DEVICE_TOKEN, repr(summary))
|
||||
self.assertEqual(protector.unprotect_calls, 0)
|
||||
|
||||
def test_profile_summary_empty_cipher_fails_closed_without_unprotect(self) -> None:
|
||||
connection = sqlite3.connect(self.database)
|
||||
try:
|
||||
connection.execute(
|
||||
"UPDATE profiles SET device_token_cipher=? WHERE profile_id=?",
|
||||
(sqlite3.Binary(b""), PROFILE),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
protector = NoUnprotectProtector()
|
||||
summary_store = LocalStateStore(self.database, protector, now=lambda: self.clock[0])
|
||||
with self.assertRaisesRegex(StateError, "invalid_device_token_cipher"):
|
||||
summary_store.load_profile_summary(PROFILE)
|
||||
self.assertEqual(protector.unprotect_calls, 0)
|
||||
|
||||
def test_profile_summary_wrong_cipher_storage_class_fails_closed_without_unprotect(self) -> None:
|
||||
current = settings()
|
||||
row = (
|
||||
current.service_url,
|
||||
current.device_id,
|
||||
current.adb_path,
|
||||
current.adb_serial,
|
||||
current.transport,
|
||||
current.poll_interval_seconds,
|
||||
current.failure_threshold,
|
||||
current.http_timeout_seconds,
|
||||
current.step_timeout_seconds,
|
||||
"text",
|
||||
64,
|
||||
)
|
||||
protector = NoUnprotectProtector()
|
||||
summary_store = LocalStateStore(self.database, protector, now=lambda: self.clock[0])
|
||||
fake_connection = mock.Mock()
|
||||
fake_connection.execute.return_value.fetchone.return_value = row
|
||||
with mock.patch.object(summary_store, "_read_transaction") as read_transaction:
|
||||
read_transaction.return_value.__enter__.return_value = fake_connection
|
||||
with self.assertRaisesRegex(StateError, "invalid_device_token_cipher"):
|
||||
summary_store.load_profile_summary(PROFILE)
|
||||
self.assertEqual(protector.unprotect_calls, 0)
|
||||
|
||||
def test_profile_summary_invalid_stored_settings_are_normalized_without_unprotect(self) -> None:
|
||||
original = settings()
|
||||
protector = NoUnprotectProtector()
|
||||
summary_store = LocalStateStore(self.database, protector, now=lambda: self.clock[0])
|
||||
for column, invalid, valid in (
|
||||
("service_url", "http://127.0.0.1:9999", original.service_url),
|
||||
("transport", "bluetooth", original.transport),
|
||||
("poll_interval_seconds", 4, original.poll_interval_seconds),
|
||||
):
|
||||
with self.subTest(column=column):
|
||||
connection = sqlite3.connect(self.database)
|
||||
try:
|
||||
connection.execute("PRAGMA ignore_check_constraints=ON")
|
||||
connection.execute(
|
||||
f"UPDATE profiles SET {column}=? WHERE profile_id=?",
|
||||
(invalid, PROFILE),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
try:
|
||||
with self.assertRaisesRegex(StateError, "stored_profile_invalid") as captured:
|
||||
summary_store.load_profile_summary(PROFILE)
|
||||
self.assertNotIn(DEVICE_TOKEN, str(captured.exception))
|
||||
finally:
|
||||
connection = sqlite3.connect(self.database)
|
||||
try:
|
||||
connection.execute("PRAGMA ignore_check_constraints=ON")
|
||||
connection.execute(
|
||||
f"UPDATE profiles SET {column}=? WHERE profile_id=?",
|
||||
(valid, PROFILE),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
self.assertEqual(protector.unprotect_calls, 0)
|
||||
|
||||
def test_profile_summary_missing_profile_fails_without_creating_defaults(self) -> None:
|
||||
with self.assertRaisesRegex(StateError, "profile_not_found"):
|
||||
|
||||
Reference in New Issue
Block a user