mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-17 10:37:12 +00:00
rebuild macos build
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""Release failure gates, private-key handling and notarization result checks."""
|
||||
from contextlib import redirect_stdout
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import release
|
||||
|
||||
FINGERPRINT = "A" * 40
|
||||
IDENTITY = "Developer ID Application: Test Only (NOTAREALID)"
|
||||
SUBMISSION = "a8c9438a-b972-455e-aedc-278070349394"
|
||||
ENVIRONMENT = {
|
||||
"MACOS_CERTIFICATE_P12_BASE64": base64.b64encode(b"not-a-real-certificate").decode(),
|
||||
"MACOS_CERTIFICATE_PASSWORD": "unit-test-password",
|
||||
"MACOS_SIGNING_IDENTITY": IDENTITY,
|
||||
"APPLE_API_KEY_P8": "-----BEGIN PRIVATE KEY-----\nunit-test-only\n-----END PRIVATE KEY-----",
|
||||
"APPLE_API_KEY_ID": "TESTKEY123",
|
||||
"APPLE_API_ISSUER_ID": SUBMISSION,
|
||||
}
|
||||
|
||||
|
||||
class ReleaseTests(unittest.TestCase):
|
||||
def credentials(self):
|
||||
with patch.dict(os.environ, ENVIRONMENT, clear=True):
|
||||
return release.Credentials.from_environment()
|
||||
|
||||
def test_config_reports_names_and_never_values(self):
|
||||
with patch.dict(os.environ, {}, clear=True), self.assertRaisesRegex(RuntimeError, "MACOS_CERTIFICATE_P12_BASE64"):
|
||||
release.Credentials.from_environment()
|
||||
|
||||
def test_config_accepts_multiline_certificate_base64(self):
|
||||
env = ENVIRONMENT.copy()
|
||||
text = env["MACOS_CERTIFICATE_P12_BASE64"]
|
||||
env["MACOS_CERTIFICATE_P12_BASE64"] = text[:10] + "\n" + text[10:]
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
self.assertEqual(release.Credentials.from_environment().certificate, b"not-a-real-certificate")
|
||||
|
||||
def test_config_rejects_development_certificate(self):
|
||||
env = {**ENVIRONMENT, "MACOS_SIGNING_IDENTITY": "Apple Development: Test"}
|
||||
with patch.dict(os.environ, env, clear=True), self.assertRaisesRegex(RuntimeError, "Developer ID Application"):
|
||||
release.Credentials.from_environment()
|
||||
|
||||
def test_config_rejects_base64_instead_of_plain_p8(self):
|
||||
env = {**ENVIRONMENT, "APPLE_API_KEY_P8": "not-the-pem-contents"}
|
||||
with patch.dict(os.environ, env, clear=True), self.assertRaisesRegex(RuntimeError, "complete, plain-text"):
|
||||
release.Credentials.from_environment()
|
||||
|
||||
def test_execute_does_not_expose_secret_argv_or_stderr(self):
|
||||
result = subprocess.CompletedProcess([], 1, stdout="", stderr="secret-password")
|
||||
with patch.object(release.subprocess, "run", return_value=result):
|
||||
with self.assertRaises(RuntimeError) as error:
|
||||
release.execute(["security", "import", "-P", "secret-password"], "Import certificate")
|
||||
self.assertNotIn("secret-password", str(error.exception))
|
||||
|
||||
def test_matching_identity_requires_developer_id_and_private_key(self):
|
||||
output = f' 1) {FINGERPRINT} "{IDENTITY}"\n'
|
||||
self.assertEqual(release.resolve_identity(output, IDENTITY), FINGERPRINT)
|
||||
with self.assertRaisesRegex(RuntimeError, "no valid Developer ID"):
|
||||
release.resolve_identity("0 valid identities found", IDENTITY)
|
||||
|
||||
def test_private_files_have_restricted_permissions(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "private.p8"
|
||||
release.write_private(path, b"test-only")
|
||||
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_keychain_and_private_files_are_cleaned_up_after_signing_failure(self):
|
||||
calls = []
|
||||
def fake_execute(args, operation, **kwargs):
|
||||
calls.append([str(value) for value in args])
|
||||
output = ''
|
||||
if args[:3] == ["security", "list-keychains", "-d"] and len(args) == 4:
|
||||
output = '"/tmp/original.keychain-db"\n'
|
||||
if args[:2] == ["security", "create-keychain"]:
|
||||
Path(args[-1]).touch()
|
||||
if args[:2] == ["security", "find-identity"]:
|
||||
output = f' 1) {FINGERPRINT} "{IDENTITY}"\n'
|
||||
return subprocess.CompletedProcess(args, 0, output, "")
|
||||
temporary_path = None
|
||||
with patch.object(release, "execute", side_effect=fake_execute), patch.object(release.urllib.request, "urlopen", side_effect=lambda *args, **kwargs: io.BytesIO(b"public-test-ca")):
|
||||
with self.assertRaisesRegex(RuntimeError, "test signing failed"):
|
||||
with release.signing_keychain(self.credentials()) as (keychain, api_key, identity):
|
||||
temporary_path = keychain.parent
|
||||
self.assertEqual(identity, FINGERPRINT)
|
||||
self.assertTrue(api_key.is_file())
|
||||
raise RuntimeError("test signing failed")
|
||||
self.assertFalse(temporary_path.exists())
|
||||
self.assertIn(["security", "list-keychains", "-d", "user", "-s", "/tmp/original.keychain-db"], calls)
|
||||
self.assertTrue(any(call[:2] == ["security", "delete-keychain"] for call in calls))
|
||||
|
||||
def run_notary(self, status, returncode=0):
|
||||
calls = []
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
directory = Path(temporary.name)
|
||||
app = directory / "ZoiteChat.app"
|
||||
app.mkdir()
|
||||
output = directory / "release.zip"
|
||||
diagnostics = directory / "diagnostics"
|
||||
def fake_execute(args, operation, **kwargs):
|
||||
calls.append([str(value) for value in args])
|
||||
text = ""
|
||||
code = 0
|
||||
if args[:3] == ["xcrun", "notarytool", "submit"]:
|
||||
text = json.dumps({"id": SUBMISSION})
|
||||
elif args[:3] == ["xcrun", "notarytool", "wait"]:
|
||||
text = json.dumps({"id": SUBMISSION, "status": status})
|
||||
code = returncode
|
||||
elif args[:3] == ["xcrun", "notarytool", "log"]:
|
||||
Path(args[-1]).write_text('{"issues": []}')
|
||||
elif args[0] == "ditto":
|
||||
Path(args[-1]).write_bytes(b"test-zip")
|
||||
return subprocess.CompletedProcess(args, code, text, "")
|
||||
with patch.object(release, "execute", side_effect=fake_execute), redirect_stdout(io.StringIO()):
|
||||
if status == "Accepted" and not returncode:
|
||||
release.notarize(app, output, diagnostics, self.credentials(), directory / "test.p8", "60m")
|
||||
else:
|
||||
with self.assertRaisesRegex(RuntimeError, "not accepted"):
|
||||
release.notarize(app, output, diagnostics, self.credentials(), directory / "test.p8", "60m")
|
||||
return calls, output, diagnostics
|
||||
|
||||
def test_accepted_notarization_staples_and_checks_before_final_zip(self):
|
||||
calls, output, diagnostics = self.run_notary("Accepted")
|
||||
self.assertTrue(output.is_file())
|
||||
staple = next(i for i, call in enumerate(calls) if call[:3] == ["xcrun", "stapler", "staple"])
|
||||
assess = next(i for i, call in enumerate(calls) if call[0] == "spctl")
|
||||
final_zip = max(i for i, call in enumerate(calls) if call[0] == "ditto")
|
||||
self.assertLess(staple, assess)
|
||||
self.assertLess(assess, final_zip)
|
||||
self.assertEqual(json.loads((diagnostics / "status.json").read_text())["status"], "Accepted")
|
||||
|
||||
def test_rejected_notarization_never_staples_or_publishes(self):
|
||||
calls, output, diagnostics = self.run_notary("Invalid")
|
||||
self.assertFalse(output.exists())
|
||||
self.assertTrue((diagnostics / "notary-log.json").is_file())
|
||||
self.assertFalse(any(call[:2] == ["xcrun", "stapler"] for call in calls))
|
||||
|
||||
def test_timeout_retains_submission_id_and_never_publishes(self):
|
||||
calls, output, diagnostics = self.run_notary("In Progress", 1)
|
||||
self.assertFalse(output.exists())
|
||||
self.assertEqual(json.loads((diagnostics / "submission.json").read_text())["id"], SUBMISSION)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user