81 lines
4.0 KiB
Python
81 lines
4.0 KiB
Python
"""Synthetic offline checks; no key files or Firebase requests are needed."""
|
|
import os
|
|
import stat
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import push_preflight
|
|
|
|
|
|
class PushPreflightTests(unittest.TestCase):
|
|
account = 'metalcircle-push-preprod@metalcircle-30d9b.iam.gserviceaccount.com'
|
|
|
|
def setUp(self):
|
|
env = patch.dict(os.environ, {
|
|
'PUSH_ENABLED': 'true', 'FIREBASE_PROJECT_ID': 'metalcircle-30d9b',
|
|
'GOOGLE_APPLICATION_CREDENTIALS': '/run/secrets/firebase-service-account.json',
|
|
})
|
|
env.start()
|
|
self.addCleanup(env.stop)
|
|
for target, value in (
|
|
('push_preflight.Path.stat', SimpleNamespace(st_mode=stat.S_IFREG | 0o600)),
|
|
('push_preflight.os.statvfs', SimpleNamespace(f_flag=os.ST_RDONLY)),
|
|
('push_preflight.credentials.Certificate', SimpleNamespace(
|
|
project_id='metalcircle-30d9b', service_account_email=self.account)),
|
|
):
|
|
mock = patch(target, return_value=value)
|
|
mock.start()
|
|
self.addCleanup(mock.stop)
|
|
|
|
def test_expected_read_only_preprod_credential_passes(self):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_rejects_local_account_even_in_the_same_project(self):
|
|
with patch('push_preflight.credentials.Certificate', return_value=SimpleNamespace(
|
|
project_id='metalcircle-30d9b',
|
|
service_account_email='metalcircle-push-local@metalcircle-30d9b.iam.gserviceaccount.com',
|
|
)):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^credential_service_account_mismatch$'):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_rejects_wrong_project(self):
|
|
with patch.dict(os.environ, {'FIREBASE_PROJECT_ID': 'wrong-project'}):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^credential_project_mismatch$'):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_rejects_missing_config_or_disabled_push(self):
|
|
for values, code in (
|
|
({'PUSH_ENABLED': 'false'}, 'push_disabled'),
|
|
({'FIREBASE_PROJECT_ID': ''}, 'project_missing'),
|
|
({'GOOGLE_APPLICATION_CREDENTIALS': '/app/key.json'}, 'container_path_mismatch'),
|
|
):
|
|
with self.subTest(code=code), patch.dict(os.environ, values):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^' + code + '$'):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_rejects_broad_permissions_and_directory_mounts(self):
|
|
for mode, code in (
|
|
(stat.S_IFREG | 0o644, 'credential_permissions_too_broad'),
|
|
(stat.S_IFDIR | 0o700, 'credential_not_a_file'),
|
|
):
|
|
with self.subTest(mode=mode), patch('push_preflight.Path.stat', return_value=SimpleNamespace(st_mode=mode)):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^' + code + '$'):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_rejects_writable_mount(self):
|
|
with patch('push_preflight.os.statvfs', return_value=SimpleNamespace(f_flag=0)):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^credential_mount_not_read_only$'):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_invalid_or_unreadable_credential_error_never_exposes_details(self):
|
|
for target in ('push_preflight.Path.stat', 'push_preflight.credentials.Certificate'):
|
|
with self.subTest(target=target), patch(target, side_effect=ValueError('sensitive SDK details')):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^credential_missing_unreadable_or_invalid$'):
|
|
push_preflight.check(self.account)
|
|
|
|
def test_missing_credential_fails_without_disclosing_path_or_sdk_details(self):
|
|
with patch('push_preflight.Path.stat', side_effect=FileNotFoundError('private filename')):
|
|
with self.assertRaisesRegex(push_preflight.PreflightError, '^credential_missing_unreadable_or_invalid$'):
|
|
push_preflight.check(self.account)
|