Coverage for chatgpt_proxy / tests / test_api.py: 99%
374 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 16:19 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 16:19 +0000
1# MIT License
2#
3# Copyright (c) 2025 Tuomo Kriikkula
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
23import asyncio
24import datetime
25import hashlib
26import ipaddress
27import logging
28import os
29from typing import AsyncGenerator
31import asyncpg
32import httpx
33import jwt
34import nest_asyncio
35import openai.types.responses as openai_responses
36import pytest
37import pytest_asyncio
38import respx
39from pytest_loguru.plugin import caplog # noqa: F401
40from sanic.log import access_logger as sanic_access_logger
41from sanic.log import logger as sanic_logger
42from sanic_testing.reusable import ReusableClient
44from chatgpt_proxy.tests import setup # noqa: E402
46setup.common_test_setup()
48# noinspection PyUnresolvedReferences
49import chatgpt_proxy # noqa: E402
50from chatgpt_proxy import auth # noqa: E402
51from chatgpt_proxy.app import app # noqa: E402
52from chatgpt_proxy.app import game_id_length # noqa: E402
53from chatgpt_proxy.app import make_api_v1_app # noqa: E402
54from chatgpt_proxy.app import max_ast_literal_eval_size # noqa: E402
55from chatgpt_proxy.app import openai_model # noqa: E402
56from chatgpt_proxy.cache import app_cache # noqa: E402
57from chatgpt_proxy.db import models # noqa: E402
58from chatgpt_proxy.db import pool_acquire # noqa: E402
59from chatgpt_proxy.db import queries # noqa: E402
60from chatgpt_proxy.db.models import SayType # noqa: E402
61from chatgpt_proxy.db.models import Team # noqa: E402
62from chatgpt_proxy.log import logger # noqa: E402
63from chatgpt_proxy.tests.client import SpoofedSanicASGITestClient # noqa: E402
64from chatgpt_proxy.tests.monkey_patch import monkey_patch_sanic_testing # noqa: E402
65from chatgpt_proxy.tests.setup import retry_context # noqa: E402
66from chatgpt_proxy.types import App # noqa: E402
67from chatgpt_proxy.utils import utcnow # noqa: E402
69logger.level("DEBUG")
71_asgi_host = "127.0.0.1"
73monkey_patch_sanic_testing(
74 asgi_host=_asgi_host,
75)
77_db_timeout = setup.default_test_db_timeout
79_game_server_address = ipaddress.IPv4Address("127.0.0.1")
80_game_server_port = 7777
81_now = utcnow()
82_iat = _now
83_exp = _now + datetime.timedelta(hours=12)
84_token = jwt.encode(
85 key=setup.test_sanic_secret,
86 algorithm="HS256",
87 payload={
88 "iss": auth.jwt_issuer,
89 "aud": auth.jwt_audience,
90 "sub": f"{_game_server_address}:{_game_server_port}",
91 "iat": int(_iat.timestamp()),
92 "exp": int(_exp.timestamp()),
93 },
94)
95_token_sha256 = hashlib.sha256(_token.encode()).digest()
96_headers: dict[str, str] = {
97 "Authorization": f"Bearer {_token}",
98}
100_token_bad_extra_metadata = jwt.encode(
101 key=setup.test_sanic_secret,
102 algorithm="HS256",
103 payload={
104 "iss": auth.jwt_issuer,
105 "aud": auth.jwt_audience,
106 "sub": f"{_game_server_address}:{_game_server_port}",
107 "iat": int(_iat.timestamp()),
108 "exp": int(_exp.timestamp()),
109 "whatthefuck": "hmm?",
110 },
111)
113_forbidden_game_server_address = ipaddress.IPv4Address("88.99.12.1")
114_forbidden_game_server_port = 6969
115_token_for_forbidden_server = jwt.encode(
116 key=setup.test_sanic_secret,
117 algorithm="HS256",
118 payload={
119 "iss": auth.jwt_issuer,
120 "aud": auth.jwt_audience,
121 "sub": f"{_forbidden_game_server_address}:{_forbidden_game_server_port}",
122 "iat": int(_iat.timestamp()),
123 "exp": int(_exp.timestamp()),
124 },
125)
126_token_for_forbidden_server_sha256 = hashlib.sha256(
127 _token_for_forbidden_server.encode()).digest()
129_steam_web_api_get_server_list_dummy_filter = (
130 f"\\gamedir\\rs2\\gameaddr\\{_game_server_address}:{_game_server_port}")
132app.config.ACCESS_LOG = True
133app.config.OAS = False
134app.config.OAS_AUTODOC = False
135sanic_logger.setLevel(logging.DEBUG)
136sanic_access_logger.setLevel(logging.DEBUG)
138# TODO: maybe just use namedtuple?
139ApiFixtureTuple = tuple[
140 App,
141 ReusableClient,
142 respx.MockRouter,
143 respx.MockRouter,
144 asyncpg.Connection,
145]
148def patch_openai_response_output_text(
149 mock_router: respx.MockRouter,
150 output_text: str,
151 method: str,
152 status_code: int = 200,
153):
154 response = openai_responses.Response(
155 id="testing_0",
156 model=openai_model,
157 created_at=utcnow().timestamp(),
158 object="response",
159 error=None,
160 instructions=None,
161 parallel_tool_calls=False,
162 tool_choice="auto",
163 tools=[],
164 output=[
165 openai_responses.ResponseOutputMessage(
166 id="msg_0_testing_0",
167 content=[
168 openai_responses.ResponseOutputText(
169 annotations=[],
170 text=output_text,
171 type="output_text",
172 ),
173 ],
174 role="assistant",
175 status="completed",
176 type="message",
177 ),
178 ],
179 )
181 meth = getattr(mock_router, method)
182 meth("/v1/responses").mock(
183 return_value=httpx.Response(
184 status_code=status_code,
185 json=response.model_dump(mode="json"),
186 ))
189@pytest_asyncio.fixture
190async def api_fixture(
191) -> AsyncGenerator[ApiFixtureTuple]:
192 loop = asyncio.get_running_loop()
193 nest_asyncio.apply(loop=loop)
195 db_fixture_pool = await asyncpg.create_pool(
196 dsn=setup.db_base_url,
197 min_size=1,
198 max_size=1,
199 timeout=_db_timeout,
200 loop=loop,
201 )
203 async with pool_acquire(db_fixture_pool, timeout=_db_timeout) as conn:
204 await setup.drop_test_db(conn, timeout=_db_timeout)
205 await setup.create_test_db(conn, timeout=_db_timeout)
207 test_db_pool = await asyncpg.create_pool(
208 dsn=setup.db_test_url,
209 min_size=1,
210 max_size=1,
211 timeout=_db_timeout,
212 loop=loop,
213 )
215 async with pool_acquire(
216 test_db_pool,
217 timeout=_db_timeout,
218 ) as conn:
219 async with conn.transaction():
220 await setup.initialize_test_db(conn, timeout=_db_timeout)
221 await setup.seed_test_db(conn, timeout=_db_timeout)
223 await queries.insert_game_server_api_key(
224 conn=conn,
225 issued_at=_iat,
226 expires_at=_exp,
227 token_hash=_token_sha256,
228 game_server_address=_game_server_address,
229 game_server_port=_game_server_port,
230 name="pytest API key",
231 )
232 await queries.insert_game_server_api_key(
233 conn=conn,
234 issued_at=_iat,
235 expires_at=_exp,
236 token_hash=_token_for_forbidden_server_sha256,
237 game_server_address=_forbidden_game_server_address,
238 game_server_port=_forbidden_game_server_port,
239 name="pytest API key (forbidden game server)",
240 )
242 app.asgi_client.headers = _headers
243 app.asgi_client.loop = loop
245 with (respx.MockRouter(
246 base_url="https://api.openai.com/",
247 assert_all_called=False,
248 ) as openai_mock_router,
249 respx.MockRouter(
250 base_url="https://api.steampowered.com",
251 assert_all_called=False,
252 ) as steam_web_api_mock_router
253 ):
254 openai_mock_router.route(host=_asgi_host).pass_through()
255 patch_openai_response_output_text(
256 mock_router=openai_mock_router,
257 output_text="This is a mocked test message!",
258 method="post",
259 )
261 steam_web_api_mock_router.route(host=_asgi_host).pass_through()
262 steam_web_api_mock_router.get(
263 "IGameServersService/GetServerList/v1/",
264 params={
265 "key": setup.steam_web_api_key,
266 "filter": _steam_web_api_get_server_list_dummy_filter,
267 }
268 ).mock(
269 return_value=httpx.Response(
270 status_code=200,
271 json={
272 "response": {
273 "servers": [
274 {
275 "addr": "127.0.0.1:27015",
276 "gameport": 7777,
277 "steamid": "43215698745632158",
278 "name": "Dummy Server for pytest",
279 "appid": 418460,
280 "gamedir": "RS2",
281 "version": "1094",
282 "product": "RS2",
283 "region": 255,
284 "players": 3,
285 "max_players": 64,
286 "bots": 0,
287 "map": "VNSK-Riverbed",
288 "secure": True,
289 "dedicated": True,
290 "os": "w",
291 "gametype": "does_not_matter"
292 },
293 ],
294 },
295 },
296 ))
298 def retry_cb(exc: Exception, retry: int) -> None:
299 print("#" * 500) # TODO: remove me!
300 logger.info(
301 "reusable_client: retry attempt {}: error: {}: {}",
302 retry, type(exc).__name__, exc,
303 )
305 # NOTE: can't reuse the same app for ReusableClient!
306 reusable_app = make_api_v1_app(
307 "ChatGPTProxy-Reusable",
308 )
310 def client_builder() -> ReusableClient:
311 return ReusableClient(
312 reusable_app,
313 host=_asgi_host,
314 loop=loop,
315 client_kwargs={
316 "headers": _headers,
317 }
318 )
320 with retry_context(
321 client_builder,
322 retries=5,
323 delay=datetime.timedelta(milliseconds=500),
324 exc_types=(PermissionError,),
325 retry_cb=retry_cb,
326 ) as reusable_client:
327 yield app, reusable_client, openai_mock_router, steam_web_api_mock_router, conn
329 async with pool_acquire(db_fixture_pool, timeout=_db_timeout) as conn:
330 await setup.drop_test_db(conn, timeout=_db_timeout)
332 await db_fixture_pool.close()
333 await test_db_pool.close()
336@pytest.mark.asyncio
337async def test_api_v1_post_game(api_fixture, caplog) -> None:
338 caplog.set_level(logging.DEBUG)
339 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
341 data = "VNTE-TestSuite\nTest Suite\n7777"
342 req, resp = reusable_client.post("/api/v1/game", data=data)
343 assert resp.status == 201
345 game_id, greeting = resp.text.split("\n")
346 assert len(game_id) == game_id_length * 2 # Num bytes as hex string.
348 req, resp = reusable_client.get(f"/api/v1/game/{game_id}")
349 assert resp.status == 200
350 game = resp.json
351 assert game
353 # Empty data.
354 data = ""
355 req, resp = reusable_client.post("/api/v1/game", data=data)
356 assert resp.status == 400
358 # Bad data.
359 data = "dsflkjgjknoe8923u58u234r02opkwepkf\n\r"
360 req, resp = reusable_client.post("/api/v1/game", data=data)
361 assert resp.status == 400
364@pytest.mark.asyncio
365async def test_api_v1_put_game(api_fixture, caplog) -> None:
366 caplog.set_level(logging.DEBUG)
367 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
369 # Valid request.
370 world_time = 548.8584
371 data = f"{world_time}"
372 req, resp = reusable_client.put("/api/v1/game/first_game", data=data)
373 assert resp.status == 204
375 # Bad data -> 400.
376 bad_world_time = "this is not a float"
377 data = f"{bad_world_time}"
378 req, resp = reusable_client.put("/api/v1/game/first_game", data=data)
379 assert resp.status == 400
381 # Non-existent game.
382 data = "this doesn't matter in this case!"
383 req, resp = reusable_client.put("/api/v1/game/asdasdasd1243", data=data)
384 assert resp.status == 404
387@pytest.mark.asyncio
388async def test_api_v1_post_game_invalid_token(api_fixture, caplog) -> None:
389 caplog.set_level(logging.DEBUG)
390 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
392 # No token.
393 logger.info("testing no token")
394 data = "VNTE-TestSuite\n7777"
395 req, resp = reusable_client.post(
396 "/api/v1/game",
397 data=data,
398 headers={
399 "Authorization": "",
400 },
401 )
402 assert resp.status == 401
404 # Bad token (garbage data).
405 logger.info("testing bad token")
406 data = "VNTE-TestSuite\n7777"
407 req, resp = reusable_client.post(
408 "/api/v1/game",
409 data=data,
410 headers={
411 "Authorization": "Bearer aasddsadasasdadsadsdsaasdasdads",
412 },
413 )
414 assert resp.status == 401
416 # Good token with unwanted extra claims added
417 # -> token hash check mismatch with database-stored hash.
418 logger.info("testing token with extra metadata")
419 data = "VNTE-TestSuite\n7777"
420 req, resp = reusable_client.post(
421 "/api/v1/game",
422 data=data,
423 headers={
424 "Authorization": f"Bearer {_token_bad_extra_metadata}",
425 },
426 )
427 assert resp.status == 401
429 logger.info("testing Steam not recognizing the dedicated server")
430 with steam_mock_router:
431 steam_mock_router.get(
432 "IGameServersService/GetServerList/v1/",
433 params={
434 "key": setup.steam_web_api_key,
435 "filter": _steam_web_api_get_server_list_dummy_filter,
436 }
437 ).mock(
438 return_value=httpx.Response(
439 status_code=200,
440 json={
441 "response": {
442 "servers": [],
443 },
444 },
445 ))
446 data = "VNTE-WhatTheFuckBro\n7777"
447 req_, resp_ = reusable_client.post("/api/v1/game", data=data)
448 assert resp_.status == 401, resp_.body
450 logger.info("testing Steam API returning garbage")
451 with steam_mock_router:
452 steam_mock_router.get(
453 "IGameServersService/GetServerList/v1/",
454 params={
455 "key": setup.steam_web_api_key,
456 "filter": _steam_web_api_get_server_list_dummy_filter,
457 }
458 ).mock(
459 return_value=httpx.Response(
460 status_code=200,
461 json={
462 "blasdalsd": {
463 "xorvors": [],
464 },
465 },
466 ))
467 data = "VNTE-WhatTheFuckBro\n7777"
468 req, resp = reusable_client.post("/api/v1/game", data=data)
469 assert resp.status == 401
471 logger.info("testing token meant for another IP address (but the token exists in the DB)")
472 data = "VNTE-ThisDoesntMatterLol\n7777"
473 req, resp = reusable_client.post(
474 "/api/v1/game",
475 data=data,
476 headers={
477 "Authorization": f"Bearer {_token_for_forbidden_server}",
478 },
479 )
480 assert resp.status == 401
482 logger.info("testing token meant for another IP address")
483 spoofed_asgi_client = SpoofedSanicASGITestClient(
484 app=api_app,
485 client_ip="6.0.28.175",
486 )
487 somebody_elses_token = jwt.encode(
488 key=setup.test_sanic_secret,
489 algorithm="HS256",
490 payload={
491 "iss": auth.jwt_issuer,
492 "aud": auth.jwt_audience,
493 "sub": "6.0.28.175:55555", # Also, this does not exist in the DB.
494 "iat": int(_iat.timestamp()),
495 "exp": int(_exp.timestamp()),
496 },
497 )
498 spoofed_asgi_client.headers = {
499 "Authorization": f"Bearer {somebody_elses_token}",
500 }
501 data = "VNTE-TestSuite\n55555"
502 # noinspection PyTypeChecker
503 req, resp = await spoofed_asgi_client.post("/api/v1/game", data=data)
504 assert resp.status == 401
507@pytest.mark.asyncio
508async def test_api_v1_post_game_chat_message(api_fixture, caplog) -> None:
509 # TODO: maybe just parametrize this test.
511 caplog.set_level(logging.DEBUG)
512 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
514 path = "/api/v1/game/first_game/chat_message"
515 data = models.GameChatMessage(
516 id=0,
517 sender_name="my name is dog69",
518 sender_team=Team.North,
519 channel=SayType.ALL,
520 message="this is the actual message!",
521 game_id="first_game",
522 send_time=utcnow(),
523 ).wire_format()
524 req, resp = reusable_client.post(path, data=data)
525 assert resp.status == 204
527 path_404 = "/api/v1/game/THIS_GAME_DOES_NOT_EXIST/chat_message"
528 data = "my name is dog69\n0\n0\nthis is the actual message!"
529 req, resp = reusable_client.post(path_404, data=data)
530 assert resp.status == 404
532 path_forbidden = "/api/v1/game/game_from_forbidden_server/chat_message"
533 data = "my name is dog69\n0\n0\nthis is the actual message!"
534 req, resp = reusable_client.post(path_forbidden, data=data)
535 assert resp.status == 401
537 path = "/api/v1/game/first_game/chat_message"
538 invalid_data = "dsfsdsfdsffdsfsdsdf"
539 req, resp = reusable_client.post(path, data=invalid_data)
540 assert resp.status == 400
542 path = "/api/v1/game/first_game/chat_message"
543 empty_data = ""
544 req, resp = reusable_client.post(path, data=empty_data)
545 assert resp.status == 400
547 path = "/api/v1/game/first_game/chat_message"
548 req, resp = reusable_client.post(path) # No data.
549 assert resp.status == 400
551 # Steam Web API key is not set -> the result should still be the same,
552 # only with a warning logged, but don't bother asserting the log message.
553 # Run this check for coverage.
554 try:
555 # Make sure there aren't any cached Steam Web API results.
556 await app_cache.clear()
557 del os.environ["STEAM_WEB_API_KEY"]
558 chatgpt_proxy.auth.load_config()
559 path = "/api/v1/game/first_game/chat_message"
560 data = "my name is dog69\n0\n0\nthis is the actual message!"
561 req, resp = reusable_client.post(path, data=data)
562 assert resp.status == 204
563 finally:
564 os.environ["STEAM_WEB_API_KEY"] = setup.steam_web_api_key
565 chatgpt_proxy.auth.load_config()
567 num_steam_web_api_queries = await queries.select_steam_web_api_queries(
568 conn=db_conn,
569 )
570 assert num_steam_web_api_queries > 0
573# TODO: make a dedicated test suite for "end-to-end" tests with
574# a 'real' production setup?
575# @pytest.mark.asyncio
576# async def test_database_maintenance(api_fixture, caplog) -> None:
577# pass
580@pytest.mark.asyncio
581async def test_api_v1_put_delete_game_player(api_fixture, caplog) -> None:
582 caplog.set_level(logging.DEBUG)
583 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
585 # PUT a new player -> should be 201 CREATED.
586 new_player_id = 69696
587 path = f"/api/v1/game/first_game/player/{new_player_id}"
588 data = "Bob\n1\n-50"
589 req, resp = reusable_client.put(path, data=data)
590 assert resp.status == 201
591 player = await queries.select_game_player(
592 conn=db_conn,
593 game_id="first_game",
594 player_id=new_player_id,
595 )
596 assert player == models.GamePlayer(
597 game_id="first_game",
598 id=new_player_id,
599 name="Bob",
600 team=Team.South,
601 score=-50,
602 )
604 # PUT existing player -> should be 204 NO CONTENT.
605 data = models.GamePlayer(
606 game_id="first_game",
607 id=new_player_id,
608 name="Bob",
609 team=Team.South,
610 score=6969,
611 ).wire_format()
612 path = f"/api/v1/game/first_game/player/{new_player_id}"
613 req, resp = reusable_client.put(path, data=data)
614 assert resp.status == 204
615 player = await queries.select_game_player(
616 conn=db_conn,
617 game_id="first_game",
618 player_id=new_player_id,
619 )
620 assert player == models.GamePlayer(
621 game_id="first_game",
622 id=new_player_id,
623 name="Bob",
624 team=Team.South,
625 score=6969,
626 )
628 # Delete existing -> NO CONTENT.
629 path = f"/api/v1/game/first_game/player/{new_player_id}"
630 req, resp = reusable_client.delete(path)
631 assert resp.status == 204
633 # Second delete, should already be gone -> 404.
634 path = f"/api/v1/game/first_game/player/{new_player_id}"
635 req, resp = reusable_client.delete(path)
636 assert resp.status == 404
638 # Delete player that never existed -> 404.
639 path = "/api/v1/game/first_game/player/6904234"
640 req, resp = reusable_client.delete(path)
641 assert resp.status == 404
642 player = await queries.select_game_player(
643 conn=db_conn,
644 game_id="first_game",
645 player_id=6904234,
646 )
647 assert player is None
649 # Put with invalid ID (not int) -> 404 (Sanic logic).
650 path = "/api/v1/game/first_game/player/asdasd"
651 req, resp = reusable_client.put(path)
652 assert resp.status == 404
654 # Put with invalid data.
655 data = "asdasdasd\n\n\n\n"
656 path = "/api/v1/game/first_game/player/0"
657 req, resp = reusable_client.put(path, data=data)
658 assert resp.status == 400
661@pytest.mark.asyncio
662async def test_api_v1_put_game_objective_state(api_fixture, caplog) -> None:
663 caplog.set_level(logging.DEBUG)
664 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
666 # Empty data -> 400.
667 data = ""
668 path = "/api/v1/game/first_game/objective_state"
669 req, resp = reusable_client.put(path, data=data)
670 assert resp.status == 400
672 # Empty list (first request) -> clears state -> 201.
673 data = "[]"
674 path = "/api/v1/game/first_game/objective_state"
675 req, resp = reusable_client.put(path, data=data)
676 assert resp.status == 201
678 # Empty list (subsequent request) -> clears state -> 204.
679 data = "[]"
680 path = "/api/v1/game/first_game/objective_state"
681 req, resp = reusable_client.put(path, data=data)
682 assert resp.status == 204
684 # Valid list (subsequent request) -> 204.
685 neutral_team = int(Team.Neutral)
686 data = f"[('BlaBla',0),('SomeObjective',1),('Neutral Objective',{neutral_team})]"
687 path = "/api/v1/game/first_game/objective_state"
688 req, resp = reusable_client.put(path, data=data)
689 assert resp.status == 204
691 # Valid list (subsequent request) -> 2024.
692 data = models.GameObjectiveState(
693 game_id="first_game",
694 objectives=[
695 models.GameObjective(
696 name="BlaBla",
697 team_state=models.Team.North,
698 ),
699 models.GameObjective(
700 name="AnotherObjective",
701 team_state=models.Team.South,
702 ),
703 models.GameObjective(
704 name="THIS IS SOME WEIRD OBJECTIVE 123123",
705 team_state=models.Team.Neutral,
706 ),
707 ]
708 ).wire_format()
709 path = "/api/v1/game/first_game/objective_state"
710 req, resp = reusable_client.put(path, data=data)
711 assert resp.status == 204
713 # Bad data -> 400.
714 data = "asdasd."
715 path = "/api/v1/game/first_game/objective_state"
716 req, resp = reusable_client.put(path, data=data)
717 assert resp.status == 400
719 # Bad data -> 400.
720 data = "[(),()]"
721 path = "/api/v1/game/first_game/objective_state"
722 req, resp = reusable_client.put(path, data=data)
723 assert resp.status == 400
725 # Bad data -> 400.
726 data = "["
727 path = "/api/v1/game/first_game/objective_state"
728 req, resp = reusable_client.put(path, data=data)
729 assert resp.status == 400
731 # Bad data -> 400.
732 data = "[)"
733 path = "/api/v1/game/first_game/objective_state"
734 req, resp = reusable_client.put(path, data=data)
735 assert resp.status == 400
737 # Too much data -> 400.
738 data = "*" * (max_ast_literal_eval_size + 1)
739 path = "/api/v1/game/first_game/objective_state"
740 req, resp = reusable_client.put(path, data=data)
741 assert resp.status == 400
743 # Valid but wrong Python object.
744 data = "()"
745 path = "/api/v1/game/first_game/objective_state"
746 req, resp = reusable_client.put(path, data=data)
747 assert resp.status == 400
749 # Valid but wrong Python object.
750 data = "[(1,1)]"
751 path = "/api/v1/game/first_game/objective_state"
752 req, resp = reusable_client.put(path, data=data)
753 assert resp.status == 400
755 # Valid but wrong Python object.
756 data = "[('ValidString','this_should_be_an_int')]"
757 path = "/api/v1/game/first_game/objective_state"
758 req, resp = reusable_client.put(path, data=data)
759 assert resp.status == 400
761 # Valid but wrong Python object.
762 invalid_team_as_int = 696969
763 data = f"[('ValidString',{invalid_team_as_int})]"
764 path = "/api/v1/game/first_game/objective_state"
765 req, resp = reusable_client.put(path, data=data)
766 assert resp.status == 400
769@pytest.mark.asyncio
770async def test_api_v1_post_game_kill(api_fixture, caplog) -> None:
771 caplog.set_level(logging.DEBUG)
772 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
774 # Empty data -> 400.
775 data = ""
776 path = "/api/v1/game/first_game/kill"
777 req, resp = reusable_client.post(path, data=data)
778 assert resp.status == 400
780 # Bad data -> 400.
781 data = "\n\n\nasd\n"
782 path = "/api/v1/game/first_game/kill"
783 req, resp = reusable_client.post(path, data=data)
784 assert resp.status == 400
786 # Valid request.
787 data = "353.4503560\nSome guy lmao\nI'mDead:(\n0\n1\nRODmgType_SomeTypeLol\n88.53"
788 path = "/api/v1/game/first_game/kill"
789 req, resp = reusable_client.post(path, data=data)
790 assert resp.status == 204
791 kills = await queries.select_game_kills(conn=db_conn, game_id="first_game")
792 assert kills
794 # Valid request (teamkill).
795 data = "353.4503560\nSome guy lmao\nI'mDead:(\n1\n1\nRODmgType_SomeTypeLol\n88.53"
796 path = "/api/v1/game/first_game/kill"
797 req, resp = reusable_client.post(path, data=data)
798 assert resp.status == 204
799 kills = await queries.select_game_kills(conn=db_conn, game_id="first_game")
800 assert kills
803@pytest.mark.asyncio
804async def test_api_v1_game_message(api_fixture, caplog) -> None:
805 caplog.set_level(logging.DEBUG)
806 api_app, reusable_client, openai_mock_router, steam_mock_router, db_conn = api_fixture
808 # Non-existent game.
809 data = ""
810 path = "/api/v1/game/235235252345234234/message"
811 req, resp = reusable_client.post(path, data=data)
812 assert resp.status == 404
814 # Game was not initialized -> 503 (data does not matter here).
815 data = ""
816 path = "/api/v1/game/first_game/message"
817 req, resp = reusable_client.post(path, data=data)
818 assert resp.status == 503
820 # Initialize the game by setting a fake openai_previous_response_id.
821 # And creating the corresponding query entry.
822 await db_conn.execute(
823 """
824 UPDATE "game"
825 SET openai_previous_response_id = 'pytest_dummy_openapi_response_id'
826 WHERE id = 'first_game';
827 """
828 )
830 # Sneak in a request here -> should be 503 since the query does not exist!
831 data = ""
832 path = "/api/v1/game/first_game/message"
833 req, resp = reusable_client.post(path, data=data)
834 assert resp.status == 503
836 await db_conn.execute(
837 """
838 INSERT INTO "openai_query" (time,
839 game_id,
840 game_server_address,
841 game_server_port,
842 request_length,
843 response_length,
844 openai_response_id)
845 VALUES (NOW() AT TIME ZONE 'UTC',
846 'first_game',
847 INET '127.0.0.1',
848 7777,
849 69,
850 6969,
851 'pytest_dummy_openapi_response_id');
852 """
853 )
855 # Initialized game, bad data -> 400.
856 data = ""
857 path = "/api/v1/game/first_game/message"
858 req, resp = reusable_client.post(path, data=data)
859 assert resp.status == 400
861 output_text = "YO wtf ayo ayyo yo \n\n\nasdblasld\t!"
862 patch_openai_response_output_text(
863 mock_router=openai_mock_router,
864 output_text=output_text,
865 method="post",
866 )
867 # Initialized game, good data -> 200.
868 todo_prompt = "jksdfkljsdlkf" # TODO: PUT AN ACTUAL PROMPT HERE!
869 data = f"{SayType.ALL}\n{Team.North}\nI AM SOME GUY LOL\n{todo_prompt}"
870 path = "/api/v1/game/first_game/message"
871 req, resp = reusable_client.post(path, data=data)
872 assert resp.status == 200
873 assert resp.text.split("\n")[-1] == output_text.replace("\n", " ")
875 # Valid request, with some messages and kills belonging to the game.
876 await queries.insert_game_kill(
877 conn=db_conn,
878 game_id="first_game",
879 kill_time=utcnow(),
880 killer_name="SomeGuy69_420",
881 victim_name="Poor Non-existent Guy",
882 killer_team=Team.South,
883 victim_team=Team.North,
884 damage_type="RODmgType_RPGOrSomethingWhoCares",
885 kill_distance_m=6969.420,
886 )
887 await queries.insert_game_kill(
888 conn=db_conn,
889 game_id="first_game",
890 kill_time=utcnow(),
891 killer_name="dfgmklfdgmkldfg",
892 victim_name="+o0234uio2j3jof",
893 killer_team=Team.North,
894 victim_team=Team.North,
895 damage_type="RODmgType_XXX",
896 kill_distance_m=0.1111111111111111111111111111111,
897 )
898 await queries.insert_game_chat_message(
899 conn=db_conn,
900 game_id="first_game",
901 send_time=utcnow(),
902 sender_name="SomeGuy69_420",
903 sender_team=Team.South,
904 message="yo wtf these guys are cheating?!",
905 channel=SayType.ALL,
906 )
908 todo_prompt = "pkodfgkopdfgkop0239485" # TODO: PUT AN ACTUAL PROMPT HERE!
909 data = f"{SayType.TEAM}\n{Team.South}\nSomeFakeNameForTheAllKnowingAiHere\n{todo_prompt}"
910 path = "/api/v1/game/first_game/message"
911 req, resp = reusable_client.post(path, data=data)
912 assert resp.status == 200
913 assert resp.text.split("\n")[-1] == output_text.replace("\n", " ")