Files
AIGame/Tools/test_game_http.py
T

131 lines
8.2 KiB
Python

"""Exercise the real AIServer HTTP boundary in an isolated database/process (no account logs)."""
import concurrent.futures
import http.client
import json
from pathlib import Path
import socket
import subprocess
import tempfile
import time
import uuid
def main():
root = Path(__file__).resolve().parents[1]
exe = root / "AIServer/build-g1/Release/aiserver.exe"
with tempfile.TemporaryDirectory(prefix="g1-http-", dir=root / "Tools/output") as folder:
folder = Path(folder)
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
args = [str(exe), "--host", "127.0.0.1", "--port", str(port), "--db", str(folder / "test.db"), "--daemon"]
proc = None
def start():
nonlocal proc
proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
for _ in range(100):
if proc.poll() is not None:
raise RuntimeError("test server exited")
try:
if request("GET", "/health")[1].get("gameProtocol") == 1:
return
except OSError:
pass
time.sleep(.05)
raise RuntimeError("test server did not start")
def request(method, path, body=None, token=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
try:
payload = json.dumps(body) if body is not None else None
conn.request(method, path, payload, headers)
response = conn.getresponse()
return response.status, json.loads(response.read())
finally:
conn.close()
try:
start()
assert request("GET", "/game/state")[0] == 401
accounts = []
for name in ("alice", "bob"):
status, account = request("POST", "/register", {"username": "g1_" + name, "password": "testing-password-123"})
assert status == 200
accounts.append(account)
a, b = accounts
status, first = request("GET", "/game/state", token=a["token"])
assert status == 200 and first["state"]["balls"] == 20
command = {"requestId": uuid.uuid4().hex, "action": "shot.begin", "aim": 5, "revision": first["state"]["revision"], "pid": b["pid"]}
with concurrent.futures.ThreadPoolExecutor(max_workers=12) as pool:
results = list(pool.map(lambda _: request("POST", "/game/command", command, a["token"]), range(24)))
assert all(status == 200 and value["ok"] and value["state"]["balls"] == 19 for status, value in results)
assert request("GET", "/game/state", token=b["token"])[1]["state"]["balls"] == 20
# Replay remains valid after process restart, without trusting a supplied pid.
proc.terminate(); proc.wait(timeout=5); start()
status, replay = request("POST", "/game/command", command, a["token"])
assert status == 200 and replay["replayed"] and replay["state"]["balls"] == 19
assert replay["shot"]["path"] == results[0][1]["shot"]["path"]
changed = dict(command, aim=10)
assert request("POST", "/game/command", changed, a["token"])[0] == 409
time.sleep(max(0, replay["shot"]["readyAt"] - time.time()) + .1)
finish = {"requestId": uuid.uuid4().hex, "action": "shot.finish", "activityId": command["requestId"], "revision": replay["state"]["revision"], "coins": 999999}
status, settled = request("POST", "/game/command", finish, a["token"])
assert status == 200 and settled["ok"]
assert settled["state"]["coins"] == replay["shot"]["coins"]
assert request("POST", "/game/command", finish, a["token"])[1]["state"] == settled["state"]
assert settled['state']['schema'] == 9 and len(settled['state']['mails']) == 2 and len(settled['state']['stages']) == 10 and len(settled['state']['clearedStages']) == 20
assert settled['state']['chests'] == [0, 0, 0] and settled['state']['chestDailyClaimed'] == 0 and len(settled['state']['shards']) == 10
mail_id = settled['state']['mails'][0]['mailId']
mail = {'requestId': uuid.uuid4().hex, 'action': 'mail.claim', 'mailId': mail_id,
'revision': settled['state']['revision'], 'reward': {'coins': 999999}}
with concurrent.futures.ThreadPoolExecutor(max_workers=12) as pool:
receipts = list(pool.map(lambda _: request('POST', '/game/command', mail, a['token']), range(24)))
assert all(status == 200 and value['ok'] and value['result']['claimedCount'] == 1 for status, value in receipts)
claimed = receipts[0][1]
assert claimed['state']['balls'] == settled['state']['balls'] + 5
assert claimed['state']['candy'] == settled['state']['candy'] + 20
assert claimed['state']['coins'] == settled['state']['coins']
bob = request('GET', '/game/state', token=b['token'])[1]
assert not bob['state']['mails'][0]['claimed'] and bob['state']['balls'] == 20
proc.terminate(); proc.wait(timeout=5); start()
mail_replay = request('POST', '/game/command', mail, a['token'])[1]
assert mail_replay['replayed'] and mail_replay['result'] == claimed['result']
assert mail_replay['state']['balls'] == claimed['state']['balls']
mark = {'requestId': uuid.uuid4().hex, 'action': 'mail.read', 'mailId': mail_id, 'revision': bob['state']['revision']}
marked = request('POST', '/game/command', mark, b['token'])[1]
assert marked['ok'] and marked['state']['mails'][0]['read'] and marked['state']['balls'] == 20
# Daily chest adds a box without awarding contents; opening it awards once under retry.
daily = {'requestId': uuid.uuid4().hex, 'action': 'chest.claimDaily', 'revision': claimed['state']['revision']}
with concurrent.futures.ThreadPoolExecutor(max_workers=12) as pool:
boxes = list(pool.map(lambda _: request('POST', '/game/command', daily, a['token']), range(24)))
assert all(status == 200 and value['ok'] and value['result']['count'] == 1 for status, value in boxes)
boxed = boxes[0][1]
assert boxed['state']['chests'] == [1, 0, 0] and boxed['state']['chestDailyClaimed'] == 1
assert boxed['state']['coins'] == claimed['state']['coins'] and boxed['state']['candy'] == claimed['state']['candy']
opened = {'requestId': uuid.uuid4().hex, 'action': 'chest.open', 'chest': 0, 'revision': boxed['state']['revision']}
with concurrent.futures.ThreadPoolExecutor(max_workers=12) as pool:
receipts = list(pool.map(lambda _: request('POST', '/game/command', opened, a['token']), range(24)))
assert all(status == 200 and value['ok'] and value['result']['remaining'] == 0 for status, value in receipts)
rewarded = receipts[0][1]
assert rewarded['state']['chests'] == [0, 0, 0]
assert rewarded['state']['coins'] == boxed['state']['coins'] + 60
assert rewarded['state']['candy'] == boxed['state']['candy'] + 30
assert rewarded['state']['balls'] == boxed['state']['balls'] + 2
assert bob['state']['chests'] == [0, 0, 0] and bob['state']['chestDailyClaimed'] == 0
(folder / "maintenance.json").write_text('{"enabled":true,"message":"testing"}', encoding="utf-8")
assert request("GET", "/game/state", token=a["token"])[0] == 503
assert request("POST", "/game/command", finish, a["token"])[0] == 503
print("PASS HTTP: bearer auth, account isolation, 24 concurrent shot/mail/chest retries, restart receipts, mail read/claim, trajectory, once-only settlement, maintenance")
finally:
if proc and proc.poll() is None:
proc.terminate(); proc.wait(timeout=5)
if __name__ == "__main__":
main()