diff --git a/scripts/check_installed_server.py b/scripts/check_installed_server.py index 85b8d525..a6e0e66c 100644 --- a/scripts/check_installed_server.py +++ b/scripts/check_installed_server.py @@ -181,10 +181,13 @@ async def restore_drill(settings, folder, client, packet, source_dsn): expected_settings['recovery_quarantine'] = current_settings['recovery_quarantine'] original['tables']['settings'] = sorted(canonical(row) for row in expected_settings.items()) assert current == original, 'Restore changed other table values or sequence state' + restored_settings = load_settings(folder / 'restore-etc') + marker = restored_settings.recovery_marker + assert marker.is_file(), 'Restore did not install its configured quarantine marker' for marker_present in (True, False): if not marker_present: - (folder / 'restore-etc/recovery-drill.json').unlink() - recovered = Application(load_settings(folder / 'restore-etc')) + marker.unlink() + recovered = Application(restored_settings) try: await recovered.load() replay = await recovered.executor.execute(packet) diff --git a/src/msg/plugins/discovery.py b/src/msg/plugins/discovery.py index 1a615f4f..c3ad6004 100644 --- a/src/msg/plugins/discovery.py +++ b/src/msg/plugins/discovery.py @@ -570,8 +570,15 @@ def install(app): op, finish = registration(app, 'discovery', ('identity', 'content')) fields = {'type': 'array', 'items': STRING, 'maxItems': 30, 'uniqueItems': True} + @op( + 'discovery.read_query', + obj({'home_summary': {'const': True}}, ('home_summary',)), + effect='read', + version=4, + ) async def home(ctx, request, tx): """Live counts and recent resources, filtered by current read access.""" + require(ctx.principal.subject is None, 'public_home_summary_only') timezone = ZoneInfo('Asia/Taipei') today = ctx.now.astimezone(timezone).date() posts = posts_today = users = 0 @@ -805,16 +812,6 @@ async def read_segment(ctx, request, tx): async def list_items(ctx, request, tx, *, arguments=None, budget=None): a = dict(request.arguments if arguments is None else arguments) - if 'home_summary' in a: - require( - request.operation == 'discovery.read_query' - and request.contract_version == 1 - and a == {'home_summary': True}, - 'invalid_home_summary', - ) - require(ctx.principal.subject is None, 'public_home_summary_only') - return await home(ctx, request, tx) - stable = request.operation == 'discovery.read_query' internal_page = getattr(request, 'internal_page_state', None) if internal_page is not None: @@ -957,7 +954,7 @@ async def list_items(ctx, request, tx, *, arguments=None, budget=None): op('discovery.list', obj(listing), effect='read')(list_items) op('discovery.search', obj(listing, ('query',)), effect='read')(list_items) - op('discovery.read_query', obj({**listing, 'home_summary': BOOLEAN}), effect='read')(list_items) + op('discovery.read_query', obj(listing), effect='read')(list_items) nested_schema = obj( { diff --git a/src/msg/transports/http_routes.py b/src/msg/transports/http_routes.py index f22d285d..d1ddf7b1 100644 --- a/src/msg/transports/http_routes.py +++ b/src/msg/transports/http_routes.py @@ -1764,10 +1764,15 @@ async def dispatch(request: Request): if header: packet = path_packet(header, 'j', limits.max_request_bytes) require(packet.operation == spec.name, 'operation_mismatch') + require(packet.contract_version == spec.version, 'representation_mismatch') require(dict(packet.arguments) == args, 'representation_mismatch') else: packet = request_for( - spec.name, args, service.settings.service_url, source='manual' + spec.name, + args, + service.settings.service_url, + source='manual', + contract_version=spec.version, ) result = await service.executor.execute(packet, entry='network') value = result_wire(result) @@ -1791,6 +1796,7 @@ async def dispatch(request: Request): {'home_summary': True}, service.settings.service_url, source='manual', + contract_version=4, ) result = await service.executor.execute(packet, entry='network') if result.error and result.error.code != 'query_cost_exceeded': diff --git a/tests/test_branding.py b/tests/test_branding.py index 4ef9bb0c..3e5198f1 100644 --- a/tests/test_branding.py +++ b/tests/test_branding.py @@ -9,7 +9,9 @@ import pytest from test_service import NOW, call, register -from msg.core.codec import canonical, wire +from msg.core.codec import b64, canonical, wire +from msg.core.requests import request_for +from msg.transports.dictionary import build_dictionary from msg.transports.http import create_app @@ -217,11 +219,43 @@ async def test_home_summary_uses_existing_read_authority_and_remains_public(inst app, _ = installed assert all(operation.name != 'discovery.home' for operation in app.registry.operations()) key, uid, cert = await register(app, 'home-summary-reader') - anonymous = await call(app, 'discovery.read_query', {'home_summary': True}) + anonymous = await call(app, 'discovery.read_query', {'home_summary': True}, contract_version=4) assert anonymous.status == 'ok' and 'posts' in anonymous.data authenticated = await call( - app, 'discovery.read_query', {'home_summary': True}, key=key, subject=uid, certs=(cert,) + app, + 'discovery.read_query', + {'home_summary': True}, + key=key, + subject=uid, + certs=(cert,), + contract_version=4, ) assert authenticated.error.code == 'public_home_summary_only' - mixed = await call(app, 'discovery.read_query', {'home_summary': True, 'parent': '/main'}) - assert mixed.error.code == 'invalid_home_summary' + mixed = await call( + app, 'discovery.read_query', {'home_summary': True, 'parent': '/main'}, contract_version=4 + ) + assert mixed.error.code == 'schema_validation' + for args in ({}, {'home_summary': False}): + invalid = await call(app, 'discovery.read_query', args, contract_version=4) + assert invalid.error.code == 'schema_validation' + legacy = await call(app, 'discovery.read_query', {'home_summary': True}) + assert legacy.error.code == 'schema_validation' + dictionary = build_dictionary(app.registry) + direct_path = next( + row['example'] + for row in dictionary.document['operations'] + if row['name'] == 'discovery.read_query' and row['version'] == 4 + ) + wrong_version = request_for( + 'discovery.read_query', {'home_summary': True}, app.settings.service_url + ) + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(app)), base_url=app.settings.service_url + ) as http: + direct = await http.get(direct_path) + assert direct.status_code == 200 and 'posts' in direct.json()['data'] + mismatched = await http.get( + direct_path, headers={'X-Msg-Request': b64(canonical(wrong_version))} + ) + assert mismatched.status_code == 400 + assert mismatched.json()['error']['code'] == 'representation_mismatch' diff --git a/tests/test_nginx_secret_logs.py b/tests/test_nginx_secret_logs.py index 46a27377..fbe2185e 100644 --- a/tests/test_nginx_secret_logs.py +++ b/tests/test_nginx_secret_logs.py @@ -157,9 +157,10 @@ def recovery_drill_active(self): async def execute(self, packet, *, entry): # The homepage is the only read exercised by this boundary fixture. # Any attempted business execution still fails immediately. - assert packet.operation == 'discovery.home' + assert packet.operation == 'discovery.read_query' + assert packet.contract_version == 4 assert entry == 'network' - assert not packet.arguments + assert packet.arguments == {'home_summary': True} return SimpleNamespace( error=None, data={ diff --git a/tests/test_request_target_safety.py b/tests/test_request_target_safety.py index c18e339e..1f0c60f8 100644 --- a/tests/test_request_target_safety.py +++ b/tests/test_request_target_safety.py @@ -27,9 +27,10 @@ def recovery_drill_active(self): async def execute(self, packet, *, entry): # The homepage is the only read exercised by this boundary fixture. # Any attempted business execution still fails immediately. - assert packet.operation == 'discovery.home' + assert packet.operation == 'discovery.read_query' + assert packet.contract_version == 4 assert entry == 'network' - assert not packet.arguments + assert packet.arguments == {'home_summary': True} return SimpleNamespace( error=None, data={