Skip to content

Commit 0d68122

Browse files
authored
Merge pull request #1 from OpenMatchmaking/feature-tests-for-workers
Tests for AMQP workers
2 parents 889d6a7 + 1dd6684 commit 0d68122

File tree

9 files changed

+475
-22
lines changed

9 files changed

+475
-22
lines changed

docker-compose.dev.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ services:
4747
- MONGODB_USERNAME=user
4848
- MONGODB_PASSWORD=password
4949
- MONGODB_HOST=mongodb
50-
- MONGODB_DATABASE=auth
50+
- MONGODB_DATABASE=game_servers_pool
5151
- WAIT_FOR_MONGODB=30
5252
- WAIT_FOR_REDIS=30
5353
- WAIT_FOR_RABBITMQ=30
@@ -80,7 +80,7 @@ services:
8080
environment:
8181
- MONGODB_USERNAME=user
8282
- MONGODB_PASSWORD=password
83-
- MONGODB_DATABASE=auth
83+
- MONGODB_DATABASE=game_servers_pool
8484
- MONGODB_ROOT_PASSWORD=root
8585
networks:
8686
- app-tier

game-servers-pool/app/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from sanic_mongodb_ext import MongoDbExtension
44
from sanic_amqp_ext import AmqpExtension
55

6-
from app.workers import GetServerWorker, MicroserviceRegisterWorker, RegisterServerWorker
6+
from app.workers import GetServerWorker, RegisterServerWorker
77

88

99
app = Sanic('microservice-auth')
@@ -16,7 +16,6 @@
1616

1717
# RabbitMQ workers
1818
app.amqp.register_worker(GetServerWorker(app))
19-
app.amqp.register_worker(MicroserviceRegisterWorker(app))
2019
app.amqp.register_worker(RegisterServerWorker(app))
2120

2221

game-servers-pool/app/commands/run_server.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import asyncio
12
from sanic_script import Command, Option
23

34
from app import app
5+
from app.workers.microservice_register import MicroserviceRegisterWorker
46

57

68
class RunServerCommand(Command):
@@ -14,7 +16,15 @@ class RunServerCommand(Command):
1416
Option('--port', '-p', dest='port'),
1517
)
1618

19+
def register_microservice(self):
20+
loop = asyncio.get_event_loop()
21+
worker = MicroserviceRegisterWorker(self.app)
22+
loop.run_until_complete(worker.run(loop=loop))
23+
loop.stop()
24+
loop.close()
25+
1726
def run(self, *args, **kwargs):
27+
self.register_microservice()
1828
self.app.run(
1929
host=kwargs.get('host', None) or self.app.config["APP_HOST"],
2030
port=kwargs.get('port', None) or self.app.config["APP_PORT"],

game-servers-pool/app/game_servers/schemas.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from marshmallow import Schema, fields, validate
1+
from bson import ObjectId
2+
from marshmallow import Schema, fields, validate, validates, ValidationError
23

34
from app import app
45

@@ -23,12 +24,35 @@ class RequestGetServerSchema(Schema):
2324
)
2425

2526

26-
class GameServerSchema(GameServer.schema.as_marshmallow_schema()):
27+
class RegisterGameServerSchema(GameServer.schema.as_marshmallow_schema()):
28+
id = fields.String(required=False)
29+
30+
@validates('id')
31+
def validate_id(self, value):
32+
if not ObjectId.is_valid(value):
33+
raise ValidationError(
34+
"'{}' is not a valid ObjectId, it must be a 12-byte "
35+
"input or a 24-character hex string.".format(value)
36+
)
37+
38+
class Meta:
39+
model = GameServer
40+
fields = (
41+
'id',
42+
'host',
43+
'port',
44+
'available_slots',
45+
'credentials',
46+
'game_mode',
47+
)
48+
49+
50+
class RetrieveGameServerSchema(GameServer.schema.as_marshmallow_schema()):
2751

2852
class Meta:
2953
model = GameServer
3054
fields = (
3155
'host',
3256
'port',
33-
'credentials'
57+
'credentials',
3458
)

game-servers-pool/app/workers/get_server.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ class GetServerWorker(AmqpWorker):
1616
def __init__(self, app, *args, **kwargs):
1717
super(GetServerWorker, self).__init__(app, *args, **kwargs)
1818
from app.game_servers.documents import GameServer
19-
from app.game_servers.schemas import RequestGetServerSchema, GameServerSchema
19+
from app.game_servers.schemas import RequestGetServerSchema, RetrieveGameServerSchema
2020
self.game_server_document = GameServer
21-
self.schema = GameServerSchema
21+
self.schema = RetrieveGameServerSchema
2222
self.request_schema = RequestGetServerSchema
2323

2424
async def validate_data(self, raw_data):
@@ -46,12 +46,17 @@ async def get_game_server(self, raw_data):
4646
{'available_slots': {'$gte': data['required_slots']}},
4747
{"game_mode": data['game_mode']}
4848
]
49-
}}
49+
}},
50+
{'$sample': {'size': 1}}
5051
]
51-
instance = await self.game_server_document.collection.aggregate(pipeline).limit(1)
52-
53-
serializer = self.schema()
54-
serialized_instance = serializer.dump(instance).data
52+
result = await self.game_server_document.collection.aggregate(pipeline).to_list(1)
53+
54+
if result:
55+
instance = result[0]
56+
serializer = self.schema()
57+
serialized_instance = serializer.dump(instance).data
58+
else:
59+
serialized_instance = None
5560
return Response.with_content(serialized_instance)
5661

5762
async def process_request(self, channel, body, envelope, properties):

game-servers-pool/app/workers/register_server.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import json
2-
from uuid import uuid4
3-
42

53
from aioamqp import AmqpClosedConnection
4+
from bson import ObjectId
65
from marshmallow import ValidationError
76
from sanic_amqp_ext import AmqpWorker
87
from sage_utils.constants import VALIDATION_ERROR
@@ -18,15 +17,15 @@ class RegisterServerWorker(AmqpWorker):
1817
def __init__(self, app, *args, **kwargs):
1918
super(RegisterServerWorker, self).__init__(app, *args, **kwargs)
2019
from app.game_servers.documents import GameServer
20+
from app.game_servers.schemas import RegisterGameServerSchema
2121
self.game_server_document = GameServer
22-
self.schema = GameServer.schema.as_marshmallow_schema()
22+
self.schema = RegisterGameServerSchema
2323

2424
async def validate_data(self, raw_data):
2525
try:
2626
data = json.loads(raw_data.strip())
2727
except json.decoder.JSONDecodeError:
2828
data = {}
29-
3029
deserializer = self.schema()
3130
result = deserializer.load(data)
3231
if result.errors:
@@ -40,12 +39,12 @@ async def register_game_server(self, raw_data):
4039
except ValidationError as exc:
4140
return Response.from_error(VALIDATION_ERROR, exc.normalized_messages())
4241

43-
game_server_id = data.get('id', str(uuid4()))
42+
object_id = ObjectId(data['id']) if 'id' in data.keys() else ObjectId()
4443
await self.game_server_document.collection.replace_one(
45-
{'_id': game_server_id}, replacement=data, upsert=True
44+
{'_id': object_id}, replacement=data, upsert=True
4645
)
4746

48-
return Response.with_content({'id': game_server_id})
47+
return Response.with_content({'id': str(object_id)})
4948

5049
async def process_request(self, channel, body, envelope, properties):
5150
response = await self.register_game_server(body)
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import pytest
2+
from sage_utils.amqp.clients import RpcAmqpClient
3+
from sage_utils.constants import VALIDATION_ERROR
4+
from sage_utils.wrappers import Response
5+
6+
from app.game_servers.documents import GameServer
7+
from app.workers.get_server import GetServerWorker
8+
9+
10+
REQUEST_QUEUE = GetServerWorker.QUEUE_NAME
11+
REQUEST_EXCHANGE = GetServerWorker.REQUEST_EXCHANGE_NAME
12+
RESPONSE_EXCHANGE = GetServerWorker.RESPONSE_EXCHANGE_NAME
13+
14+
15+
async def create_game_servers(init_data_list):
16+
objects = []
17+
for create_data in init_data_list:
18+
game_server = GameServer(**create_data)
19+
await game_server.commit()
20+
objects.append(game_server)
21+
return objects
22+
23+
24+
@pytest.mark.asyncio
25+
async def test_worker_returns_one_existing_server_for_one_server_in_list(sanic_server):
26+
await GameServer.collection.delete_many({})
27+
28+
create_data = {
29+
'host': '127.0.0.1',
30+
'port': 9000,
31+
'available_slots': 100,
32+
'credentials': {
33+
'token': 'super_secret_token'
34+
},
35+
'game_mode': '1v1'
36+
}
37+
objects = await create_game_servers([create_data, ])
38+
39+
client = RpcAmqpClient(
40+
sanic_server.app,
41+
routing_key=REQUEST_QUEUE,
42+
request_exchange=REQUEST_EXCHANGE,
43+
response_queue='',
44+
response_exchange=RESPONSE_EXCHANGE
45+
)
46+
response = await client.send(payload={
47+
'required_slots': 20,
48+
'game_mode': "1v1"
49+
})
50+
51+
assert Response.EVENT_FIELD_NAME in response.keys()
52+
assert Response.CONTENT_FIELD_NAME in response.keys()
53+
content = response[Response.CONTENT_FIELD_NAME]
54+
55+
assert len(list(content.keys())) == 3
56+
assert set(content.keys()) == {'host', 'port', 'credentials'}
57+
58+
assert content['host'] == objects[0].host
59+
assert content['port'] == objects[0].port
60+
assert content['credentials'] == objects[0].credentials
61+
62+
await GameServer.collection.delete_many({})
63+
64+
65+
@pytest.mark.asyncio
66+
async def test_worker_returns_a_random_server_from_a_list(sanic_server):
67+
await GameServer.collection.delete_many({})
68+
69+
objects = await create_game_servers([
70+
{
71+
'host': '127.0.0.1',
72+
'port': 9000,
73+
'available_slots': 100,
74+
'credentials': {
75+
'token': 'super_secret_token'
76+
},
77+
'game_mode': 'team-deathmatch'
78+
},
79+
{
80+
'host': '127.0.0.1',
81+
'port': 9001,
82+
'available_slots': 50,
83+
'credentials': {
84+
'token': 'super_secret_token2'
85+
},
86+
'game_mode': 'team-deathmatch'
87+
},
88+
{
89+
'host': '127.0.0.1',
90+
'port': 9002,
91+
'available_slots': 10,
92+
'credentials': {
93+
'token': 'super_secret_token3'
94+
},
95+
'game_mode': 'team-deathmatch'
96+
},
97+
])
98+
99+
client = RpcAmqpClient(
100+
sanic_server.app,
101+
routing_key=REQUEST_QUEUE,
102+
request_exchange=REQUEST_EXCHANGE,
103+
response_queue='',
104+
response_exchange=RESPONSE_EXCHANGE
105+
)
106+
response = await client.send(payload={
107+
'required_slots': 10,
108+
'game_mode': 'team-deathmatch'
109+
})
110+
111+
assert Response.EVENT_FIELD_NAME in response.keys()
112+
assert Response.CONTENT_FIELD_NAME in response.keys()
113+
content = response[Response.CONTENT_FIELD_NAME]
114+
115+
assert len(list(content.keys())) == 3
116+
assert set(content.keys()) == {'host', 'port', 'credentials'}
117+
118+
filter_func = lambda obj: obj.host == content['host'] and obj.port == content['port'] # NOQA
119+
extracted_server = list(filter(filter_func, objects))[0]
120+
121+
assert content['host'] == extracted_server.host
122+
assert content['port'] == extracted_server.port
123+
assert content['credentials'] == extracted_server.credentials
124+
125+
await GameServer.collection.delete_many({})
126+
127+
128+
@pytest.mark.asyncio
129+
async def test_worker_returns_none_for_an_empty_list_of_servers(sanic_server):
130+
await GameServer.collection.delete_many({})
131+
132+
client = RpcAmqpClient(
133+
sanic_server.app,
134+
routing_key=REQUEST_QUEUE,
135+
request_exchange=REQUEST_EXCHANGE,
136+
response_queue='',
137+
response_exchange=RESPONSE_EXCHANGE
138+
)
139+
response = await client.send(payload={
140+
'required_slots': 10,
141+
'game_mode': 'team-deathmatch'
142+
})
143+
144+
assert Response.EVENT_FIELD_NAME in response.keys()
145+
assert Response.CONTENT_FIELD_NAME in response.keys()
146+
content = response[Response.CONTENT_FIELD_NAME]
147+
148+
assert content is None
149+
150+
await GameServer.collection.delete_many({})
151+
152+
153+
@pytest.mark.asyncio
154+
async def test_worker_returns_none_for_an_non_existing_server_type(sanic_server):
155+
await GameServer.collection.delete_many({})
156+
157+
client = RpcAmqpClient(
158+
sanic_server.app,
159+
routing_key=REQUEST_QUEUE,
160+
request_exchange=REQUEST_EXCHANGE,
161+
response_queue='',
162+
response_exchange=RESPONSE_EXCHANGE
163+
)
164+
response = await client.send(payload={
165+
'required_slots': 10,
166+
'game_mode': 'battle-royal'
167+
})
168+
169+
assert Response.EVENT_FIELD_NAME in response.keys()
170+
assert Response.CONTENT_FIELD_NAME in response.keys()
171+
content = response[Response.CONTENT_FIELD_NAME]
172+
173+
assert content is None
174+
175+
await GameServer.collection.delete_many({})
176+
177+
178+
@pytest.mark.asyncio
179+
async def test_worker_returns_a_validation_error_for_missing_fields(sanic_server):
180+
await GameServer.collection.delete_many({})
181+
182+
client = RpcAmqpClient(
183+
sanic_server.app,
184+
routing_key=REQUEST_QUEUE,
185+
request_exchange=REQUEST_EXCHANGE,
186+
response_queue='',
187+
response_exchange=RESPONSE_EXCHANGE
188+
)
189+
response = await client.send(payload={})
190+
191+
assert Response.ERROR_FIELD_NAME in response.keys()
192+
error = response[Response.ERROR_FIELD_NAME]
193+
194+
assert Response.ERROR_TYPE_FIELD_NAME in error.keys()
195+
assert error[Response.ERROR_TYPE_FIELD_NAME] == VALIDATION_ERROR
196+
197+
assert Response.ERROR_DETAILS_FIELD_NAME in error.keys()
198+
assert len(error[Response.ERROR_DETAILS_FIELD_NAME]) == 2
199+
200+
for field in ['required_slots', 'game_mode']:
201+
assert field in error[Response.ERROR_DETAILS_FIELD_NAME]
202+
assert len(error[Response.ERROR_DETAILS_FIELD_NAME][field]) == 1
203+
assert error[Response.ERROR_DETAILS_FIELD_NAME][field][0] == 'Missing data for ' \
204+
'required field.'
205+
206+
servers_count = await GameServer.collection.find().count()
207+
assert servers_count == 0
208+
209+
await GameServer.collection.delete_many({})

0 commit comments

Comments
 (0)