Prepare isolated Firebase push configuration for pre-production
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user