Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
99 lines
5.9 KiB
Python
99 lines
5.9 KiB
Python
"""Exercise receipt CLI boundaries without credentials, providers or Docker."""
|
|
from datetime import datetime
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
from unittest.mock import patch
|
|
import pytest
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
|
import restore_forgejo_offsite_locally as restore
|
|
import verify_nextcloud_offsite_restore as secondary
|
|
import scaleway_forgejo_archive as primary
|
|
import decrypt_primary_forgejo_archive as decrypt
|
|
|
|
|
|
def times(receipt):
|
|
start, finish = (datetime.fromisoformat(receipt[key]) for key in ('started_at', 'finished_at'))
|
|
assert start.tzinfo is not None and finish.tzinfo is not None
|
|
assert start <= finish
|
|
|
|
|
|
def provenance(archive):
|
|
return dict(schema='platform.real-offsite-recovery.v1',
|
|
status='offsite_fetched_pending_isolated_restore', decrypted=True,
|
|
upload_http_status=201, download_http_status=200, artifact='fixture.zip.age',
|
|
ciphertext_sha256='a'*64, plaintext_sha256=hashlib.sha256(archive.read_bytes()).hexdigest())
|
|
|
|
|
|
@pytest.mark.parametrize('case', ['success', 'cleanup_failure', 'wrong_schema', 'not_decrypted', 'wrong_hash'])
|
|
def test_restore_receipt_binds_provenance_and_finishes_after_cleanup(tmp_path, case):
|
|
archive=tmp_path/'archive.zip'; archive.write_bytes(b'fixture')
|
|
source=tmp_path/'transfer.json'; output=tmp_path/'receipt.json'
|
|
evidence=provenance(archive)
|
|
if case=='wrong_schema': evidence['schema']='other'
|
|
if case=='not_decrypted': evidence['decrypted']=False
|
|
if case=='wrong_hash': evidence['plaintext_sha256']='b'*64
|
|
source.write_text(json.dumps(evidence))
|
|
def run(archive, receipt, profile):
|
|
assert 'finished_at' not in receipt
|
|
receipt.update(status='restored',cleanup=case!='cleanup_failure')
|
|
with patch.object(sys,'argv',['restore','--archive',str(archive),'--receipt',str(output),'--transfer-receipt',str(source)]), patch.object(restore,'run',side_effect=run) as runner:
|
|
result=restore.main()
|
|
receipt=json.loads(output.read_text()); times(receipt)
|
|
assert receipt['transfer_receipt_sha256']==hashlib.sha256(source.read_bytes()).hexdigest()
|
|
assert result == (0 if case=='success' else 1)
|
|
assert receipt['status']==('restored' if case=='success' else 'failed')
|
|
if case in ('wrong_schema','not_decrypted','wrong_hash'): runner.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize('success', [False, True])
|
|
def test_secondary_records_both_terminal_outcomes(tmp_path, success):
|
|
output=tmp_path/'receipt.json'
|
|
def run(source, directory, receipt):
|
|
if not success: raise RuntimeError('PRIVATE_CANARY')
|
|
receipt['status']='offsite_fetched_pending_isolated_restore'
|
|
with patch.object(sys,'argv',['transfer','--source','unused','--directory','unused','--receipt',str(output)]), patch.object(secondary,'run',side_effect=run):
|
|
assert secondary.main()==(0 if success else 1)
|
|
receipt=json.loads(output.read_text()); times(receipt)
|
|
assert 'PRIVATE_CANARY' not in output.read_text()
|
|
|
|
|
|
def test_primary_validation_failure_has_terminal_time_without_credentials(tmp_path):
|
|
source=tmp_path/'source.json';source.write_text('{}');output=tmp_path/'receipt.json'
|
|
with patch.object(sys,'argv',['primary','--source','missing','--source-receipt',str(source),'--output',str(tmp_path/'out'),'--receipt',str(output),'--kubeconfig','unused']), patch.object(primary,'assert_cluster') as cluster:
|
|
assert primary.main()==1
|
|
cluster.assert_not_called()
|
|
times(json.loads(output.read_text()))
|
|
|
|
|
|
def test_decryption_keeps_own_schema_and_times(tmp_path):
|
|
encrypted=tmp_path/'encrypted';encrypted.write_bytes(b'fixture')
|
|
source=tmp_path/'source.json';source.write_text(json.dumps(dict(schema='platform.forgejo-primary-archive.v1',status='primary_fetched_pending_application_restore',download_hash_matches=True,ciphertext_sha256=hashlib.sha256(b'fixture').hexdigest(),started_at='2000-01-01T00:00:00Z',finished_at='2000-01-01T00:00:01Z')))
|
|
output=tmp_path/'receipt.json'
|
|
with patch.object(sys,'argv',['decrypt','--source',str(encrypted),'--output',str(tmp_path/'plain'),'--transfer-receipt',str(source),'--receipt',str(output)]), patch.object(decrypt,'bao'), patch.object(decrypt,'data',return_value={'data':{'data':{'AGE_PRIVATE_KEY':'fixture'}}}), patch.object(decrypt.subprocess,'run') as command:
|
|
command.return_value.returncode=0
|
|
assert decrypt.main()==0
|
|
receipt=json.loads(output.read_text());times(receipt)
|
|
assert receipt['schema']=='platform.forgejo-primary-decryption.v1'
|
|
assert not receipt['started_at'].startswith('2000')
|
|
assert receipt['transfer_receipt_sha256']==hashlib.sha256(source.read_bytes()).hexdigest()
|
|
|
|
|
|
@pytest.mark.parametrize('schema', ['platform.forgejo-primary-archive.v1', 'platform.forgejo-primary-decryption.v1'])
|
|
@pytest.mark.parametrize('profile', ['full', 'essentials'])
|
|
def test_primary_restore_accepts_legacy_and_distinct_decryption_receipts(tmp_path, schema, profile):
|
|
archive=tmp_path/'archive.zip';archive.write_bytes(b'fixture')
|
|
source=tmp_path/'transfer.json';output=tmp_path/'receipt.json'
|
|
source.write_text(json.dumps(dict(schema=schema,
|
|
status='primary_fetched_pending_application_restore', decrypted=True,
|
|
download_hash_matches=True, archive_profile=profile,
|
|
destination='s3://railiance-platform-pg-backup/platform-pg/application-archives/forgejo/fixture.zip.age',
|
|
ciphertext_sha256='a'*64, plaintext_sha256=hashlib.sha256(archive.read_bytes()).hexdigest())))
|
|
def run(archive, receipt, selected):
|
|
assert selected==profile
|
|
receipt.update(status='restored' if profile=='full' else 'restored_essentials',cleanup=True)
|
|
with patch.object(sys,'argv',['restore','--archive',str(archive),'--receipt',str(output),'--transfer-receipt',str(source),'--profile',profile]), patch.object(restore,'run',side_effect=run):
|
|
assert restore.main()==0
|
|
receipt=json.loads(output.read_text()); times(receipt)
|
|
assert receipt['source_provider']=='Scaleway'
|