"""Executable curl flow; standard-library JSON parsing, no Python dependencies.""" import argparse import json import pathlib import subprocess import sys import time from urllib.parse import urlencode, urlsplit scratch = pathlib.Path(sys.argv[1]) parser = argparse.ArgumentParser() parser.add_argument('--base', default='https://api.gamerspelobrasil.tech') parser.add_argument('--scenario', default='manual') parser.add_argument('--probes', action='store_true') args = parser.parse_args(sys.argv[2:]) u = urlsplit(args.base) if (u.scheme not in ('http', 'https') or not u.hostname or not args.base.startswith(u.scheme + '://') or u.scheme == 'http' and u.hostname not in ('localhost', '127.0.0.1', '::1') or u.username or u.password or u.path or u.query or u.fragment): parser.error('--base deve ser origem HTTPS exata (HTTP somente em loopback), sem caminho ou credenciais') base = args.base class Failure(Exception): def __init__(self, status, body, retry=0): self.status, self.body, self.retry = status, body, retry super().__init__(f'{status} {body.get("code", "HTTP_ERROR")}') def request(path, body=None): response, headers = scratch / 'response', scratch / 'headers' cmd = ['curl', '--silent', '--show-error', '--max-time', '6', '--noproxy', '*', '--cookie', str(scratch / 'cookies'), '--cookie-jar', str(scratch / 'cookies'), '--output', str(response), '--dump-header', str(headers), '--write-out', '%{http_code}'] if body is not None: cmd += ['--request', 'POST', '--header', 'Origin: ' + base, '--header', 'Content-Type: application/json', '--data-binary', '@-'] cmd += [base + '/api/v1/' + path] result = subprocess.run(cmd, input=json.dumps(body) if body is not None else None, text=True, capture_output=True) if result.returncode: raise Failure(0, {'code': 'NETWORK_ERROR'}) status = int(result.stdout) data = json.loads(response.read_text()) if status >= 300: retry = next((int(x.split(':', 1)[1].strip()) for x in headers.read_text().splitlines() if x.lower().startswith('retry-after:')), 0) raise Failure(status, data, retry) return data def mutation(path, body, recover): for attempt in range(3): try: return request(path, body) except Failure as error: if error.status not in (0, 429, 503) or attempt == 2: raise if error.retry: time.sleep(error.retry) saved = recover() if saved is not None: return saved time.sleep(.1 * (attempt + 1)) def player_or_none(): try: return request('players/me') except Failure as error: if error.status == 401: return None raise def main(): plans = json.loads(pathlib.Path(__file__).with_name('scenarios.json').read_text()) if args.scenario not in plans: parser.error('cenário desconhecido') plan = plans[args.scenario] mutation('sessions', {}, player_or_none) player = request('players/me') if not player['display_name']: offer = mutation('players/me/name-options', {}, lambda: None) player = mutation('players/me/name', {'option_id': offer['options'][0]['option_id']}, lambda: (lambda p: p if p['display_name'] else None)(request('players/me'))) def open_saved(): p = request('players/me') return request('runs/' + p['open_run_id']) if p['open_run_id'] else None run = mutation('runs', {'stage_id': 'missao-merenda-transparente', 'contract_version': 'journal-v1'}, open_saved) path = 'runs/' + run['run_id'] started = time.monotonic() def expect_error(events, code): try: request(path + '/events', {'events': events}) except Failure as error: assert error.body['code'] == code, str(error) return raise AssertionError('Esperava ' + code) if args.probes: expect_error([{**plan['events'][0], 'seq': 2}], 'MISSING_EVENTS') offset = 0 while offset < len(plan['events']): event = plan['events'][offset] end = offset + 1 while end < len(plan['events']) and plan['events'][end]['elapsed_ms'] == event['elapsed_ms'] and end - offset < run['limits']['batch_events'] and not (args.probes and offset == 0): end += 1 batch = plan['events'][offset:end] time.sleep(max(0, event['elapsed_ms'] / 1000 - (time.monotonic() - started))) def read_ack(): request(path) return None # Follow the read by replaying the same immutable input. run = mutation(path + '/events', {'events': batch}, read_ack) assert run['last_accepted_seq'] == batch[-1]['seq'], 'Confirmação divergente' offset = end if args.probes and event['seq'] == 1: assert request(path + '/events', {'events': [event]})['last_accepted_seq'] == 1 expect_error([{**event, 'payload': {**event['payload'], 'entity_id': 'unknown'}}], 'EVENT_CONFLICT') run = request(path) assert run['state'] == 'ready_to_finalize' and run['progress']['reason'] == plan['reason'] finish = {'last_seq': run['last_accepted_seq'], 'client_score': plan['score']} result = mutation(path + '/finish', finish, lambda: request(path)['result']) board = request('leaderboard?' + urlencode(dict(stage_id=run['stage_id'], rules_version=run['rules_version'], limit=100))) assert any(e['run_id'] == run['run_id'] for e in board['entries']), 'Publicação ausente' assert request('players/me')['player_id'] == player['player_id'] assert request(path)['result'] == result assert result['score']['total'] == plan['score'] print(json.dumps({'player': player, 'run': request(path), 'result': result}, ensure_ascii=False)) if __name__ == '__main__': try: main() except (Failure, AssertionError) as error: sys.exit(str(error))