|
| 1 | +"""adcp refuses an adcp-reporting-storage of another storage contract at startup (#1763).""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +import pytest |
| 8 | + |
| 9 | +from adcp.reporting import ReliableReportingConfigurationError |
| 10 | +from adcp.reporting.ledger import InMemoryReportingLedgerStore |
| 11 | +from adcp.reporting.ledger import storage_contract as contract |
| 12 | +from adcp.reporting.ledger.storage_contract import ( |
| 13 | + REPORTING_STORAGE_CONTRACT, |
| 14 | + declared_storage_contract, |
| 15 | + require_storage_contract, |
| 16 | + storage_contract_fingerprint, |
| 17 | +) |
| 18 | + |
| 19 | +#: The shared column lists, pinned per contract. Changing a list in |
| 20 | +#: ``_record_rows`` changes the fingerprint: bump REPORTING_STORAGE_CONTRACT, |
| 21 | +#: ship the matching adcp-reporting-storage DDL and migration, and re-pin. |
| 22 | +FINGERPRINTS = {2: "e6cb6454085a8fdc004f564f3882bcfe1a67fd9e622d72a36ba1f9d6ccd12af1"} |
| 23 | + |
| 24 | + |
| 25 | +def test_the_column_lists_are_pinned_to_the_contract_number() -> None: |
| 26 | + assert REPORTING_STORAGE_CONTRACT in FINGERPRINTS, "pin the new contract's fingerprint" |
| 27 | + assert storage_contract_fingerprint() == FINGERPRINTS[REPORTING_STORAGE_CONTRACT], ( |
| 28 | + "the shared ledger column lists changed: bump REPORTING_STORAGE_CONTRACT together " |
| 29 | + "with adcp-reporting-storage's STORAGE_CONTRACT, DDL and migration, then re-pin" |
| 30 | + ) |
| 31 | + |
| 32 | + |
| 33 | +class _Declared: |
| 34 | + storage_contract = REPORTING_STORAGE_CONTRACT |
| 35 | + |
| 36 | + |
| 37 | +class _Older: |
| 38 | + storage_contract = REPORTING_STORAGE_CONTRACT - 1 |
| 39 | + |
| 40 | + |
| 41 | +class _Newer: |
| 42 | + storage_contract = REPORTING_STORAGE_CONTRACT + 1 |
| 43 | + |
| 44 | + |
| 45 | +class _Undeclared: |
| 46 | + """A store from adcp-reporting-storage 0.1.0b4/0.2.0b0: no declaration at all.""" |
| 47 | + |
| 48 | + |
| 49 | +_Undeclared.__module__ = "adcp_reporting_storage.ledger.pg" |
| 50 | + |
| 51 | + |
| 52 | +class _Custom: |
| 53 | + """An adopter's own store, outside the check.""" |
| 54 | + |
| 55 | + |
| 56 | +@pytest.mark.parametrize( |
| 57 | + ("store", "expected"), |
| 58 | + [(_Declared(), 2), (_Older(), 1), (_Newer(), 3), (_Undeclared(), 1), (_Custom(), None)], |
| 59 | + ids=["declared", "older", "newer", "undeclared-storage-release", "custom"], |
| 60 | +) |
| 61 | +def test_a_store_is_classified_by_declaration_or_provenance( |
| 62 | + store: Any, expected: int | None |
| 63 | +) -> None: |
| 64 | + assert declared_storage_contract(store) == expected |
| 65 | + |
| 66 | + |
| 67 | +def test_the_in_memory_store_and_custom_stores_pass() -> None: |
| 68 | + require_storage_contract(InMemoryReportingLedgerStore()) |
| 69 | + require_storage_contract(_Custom()) |
| 70 | + require_storage_contract(_Declared()) |
| 71 | + |
| 72 | + |
| 73 | +def test_a_storage_release_of_an_older_contract_is_refused_with_the_fix() -> None: |
| 74 | + with pytest.raises(ReliableReportingConfigurationError) as refused: |
| 75 | + require_storage_contract(_Undeclared()) |
| 76 | + error = refused.value |
| 77 | + assert error.code == "REPORTING_SCHEMA_BEHIND" and error.recovery == "correctable" |
| 78 | + assert error.config_field == "adcp-reporting-storage" |
| 79 | + message = str(error) |
| 80 | + assert f"storage contract {REPORTING_STORAGE_CONTRACT}" in message |
| 81 | + assert "implements contract 1" in message |
| 82 | + assert "Install adcp-reporting-storage>=0.2.0b1" in message |
| 83 | + assert 'run "python -m adcp migrate"' in message and "create_schema()" in message |
| 84 | + |
| 85 | + |
| 86 | +def test_a_storage_release_of_a_newer_contract_is_refused_too() -> None: |
| 87 | + with pytest.raises(ReliableReportingConfigurationError, match="implements contract 3"): |
| 88 | + require_storage_contract(_Newer()) |
| 89 | + |
| 90 | + |
| 91 | +async def test_service_startup_refuses_the_pair_before_touching_the_schema( |
| 92 | + monkeypatch: pytest.MonkeyPatch, |
| 93 | +) -> None: |
| 94 | + from adcp.reporting.service import ReliableReportingService |
| 95 | + from tests.test_reliable_reporting_service import NOW, _account_context |
| 96 | + |
| 97 | + store = InMemoryReportingLedgerStore(clock=lambda: NOW) |
| 98 | + monkeypatch.setattr(InMemoryReportingLedgerStore, "storage_contract", 1, raising=False) |
| 99 | + touched = [] |
| 100 | + |
| 101 | + async def create_schema() -> None: |
| 102 | + touched.append(True) |
| 103 | + |
| 104 | + monkeypatch.setattr(store, "create_schema", create_schema) |
| 105 | + service = ReliableReportingService( |
| 106 | + store=store, clock=lambda: NOW, account_context=_account_context |
| 107 | + ) |
| 108 | + with pytest.raises(ReliableReportingConfigurationError) as refused: |
| 109 | + await service.initialize() |
| 110 | + assert refused.value.code == "REPORTING_SCHEMA_BEHIND" |
| 111 | + assert touched == [], "the schema must not be created or upgraded on a refused pair" |
| 112 | + |
| 113 | + |
| 114 | +def test_postgres_storage_refuses_the_pair_at_construction(monkeypatch: pytest.MonkeyPatch) -> None: |
| 115 | + import adcp.storage as facade |
| 116 | + from adcp.storage import ReportingStorage |
| 117 | + |
| 118 | + def build(**kwargs: Any) -> ReportingStorage: |
| 119 | + return ReportingStorage( |
| 120 | + durability="durable", ledger=_Undeclared(), principals=object(), subscriptions=object() |
| 121 | + ) |
| 122 | + |
| 123 | + monkeypatch.setattr(facade, "_plugin", lambda group, name: build) |
| 124 | + with pytest.raises(ReliableReportingConfigurationError, match="implements contract 1"): |
| 125 | + facade.postgres_storage(dsn="postgresql://unused") |
| 126 | + |
| 127 | + |
| 128 | +def test_the_installed_storage_distribution_declares_this_contract() -> None: |
| 129 | + pytest.importorskip("adcp_reporting_storage") |
| 130 | + from adcp_reporting_storage.contract import STORAGE_CONTRACT, adcp_storage_contract |
| 131 | + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore |
| 132 | + |
| 133 | + assert STORAGE_CONTRACT == REPORTING_STORAGE_CONTRACT |
| 134 | + assert adcp_storage_contract() == REPORTING_STORAGE_CONTRACT # storage reads our declaration |
| 135 | + assert declared_storage_contract.__module__ == contract.__name__ |
| 136 | + assert PgReportingLedgerStore.storage_contract == REPORTING_STORAGE_CONTRACT |
0 commit comments