Skip to content

Commit ec62d86

Browse files
authored
Improve tests' quality & coverage (#352)
* Add tests for `_scrub_url`. I ran `pytest -x tests --cov=socketshark --cov-report=html` and noticed that `_scrub_url` was not sufficiently tested. Now it is. * Use hard-coded values instead of reusing constants. Yes, this makes the tests more tedious to update if you merely change the copy of an error message, but that's a feature, not a bug. You *should* update the tests when you change the copy and thus acknowledge that the change you're making is intentional and works well in all (presumably tested) scenarios. In practice though, error copy barely ever changes and what you get instead with hard-coded values is much better readability of tests. * Add tests for `socketshark.metrics`. That's another module that was sorely missing tests. The coverage for `socketshark/metrics/__init__.py` jumped from 63% to 90% with these changes. * Add `.coverage` to `.gitignore`. We don't need this tracked and it's distracting to see it in `git status` whenver you play with pytest-cov locally. * Test `LogMetrics` more thoroughly. This bumps the coverage on `socketshark/metrics/log.py` from 0% on master to 70% prior to this commit to now 95%. The total coverage for all of `socketshark/` has reached 90% now. * 2 extra tests for the `on_service_event` handler. This bumps the coverage of socketshark/session.py` from 84% to 90%.
1 parent b2353d3 commit ec62d86

4 files changed

Lines changed: 218 additions & 33 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
.DS_Store
44
build/
55
.cache/
6+
.coverage
67
dist/
78
*.egg-info/
89
.tox/
@@ -11,4 +12,4 @@ venv
1112
.venv
1213
CLAUDE.local.md
1314
.claude/settings.local.json
14-
.claude/worktrees/
15+
.claude/worktrees/

tests/test_basic.py

Lines changed: 98 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@
1212
from structlog.testing import capture_logs
1313
from yarl import URL
1414

15-
from socketshark import (
16-
SocketShark,
17-
config_defaults,
18-
constants as c,
19-
setup_logging,
20-
)
15+
from socketshark import SocketShark, config_defaults, setup_logging
2116
from socketshark.session import Session
2217

2318
LOCAL_REDIS_HOST = os.environ.get('LOCAL_REDIS_HOST')
@@ -193,39 +188,39 @@ async def test_invalid_message(self):
193188
await session.on_client_event(None)
194189
assert client.log.pop() == {
195190
'status': 'error',
196-
'error': c.ERR_INVALID_EVENT,
191+
'error': 'Messages must be JSON and contain an event field.',
197192
}
198193

199194
await session.on_client_event('hello')
200195
assert client.log.pop() == {
201196
'status': 'error',
202-
'error': c.ERR_INVALID_EVENT,
197+
'error': 'Messages must be JSON and contain an event field.',
203198
}
204199

205200
await session.on_client_event({})
206201
assert client.log.pop() == {
207202
'status': 'error',
208-
'error': c.ERR_INVALID_EVENT,
203+
'error': 'Messages must be JSON and contain an event field.',
209204
}
210205

211206
await session.on_client_event({'event': None})
212207
assert client.log.pop() == {
213208
'status': 'error',
214-
'error': c.ERR_INVALID_EVENT,
209+
'error': 'Messages must be JSON and contain an event field.',
215210
}
216211

217212
await session.on_client_event({'event': ''})
218213
assert client.log.pop() == {
219214
'status': 'error',
220215
'event': '',
221-
'error': c.ERR_EVENT_NOT_FOUND,
216+
'error': 'Event not found.',
222217
}
223218

224219
await session.on_client_event({'event': 'hello'})
225220
assert client.log.pop() == {
226221
'status': 'error',
227222
'event': 'hello',
228-
'error': c.ERR_EVENT_NOT_FOUND,
223+
'error': 'Event not found.',
229224
}
230225

231226
assert not client.log
@@ -243,7 +238,7 @@ async def test_auth_invalid(self):
243238
assert client.log.pop() == {
244239
'status': 'error',
245240
'event': 'auth',
246-
'error': c.ERR_AUTH_UNSUPPORTED,
241+
'error': 'Authentication method unsupported.',
247242
}
248243

249244
no_auth_config = TEST_CONFIG.copy()
@@ -255,7 +250,7 @@ async def test_auth_invalid(self):
255250
assert client.log.pop() == {
256251
'status': 'error',
257252
'event': 'auth',
258-
'error': c.ERR_AUTH_UNSUPPORTED,
253+
'error': 'Authentication method unsupported.',
259254
}
260255
assert not client.log
261256

@@ -286,6 +281,77 @@ async def test_auth_invalid_emits_structlog_logs(self):
286281
},
287282
]
288283

284+
@pytest.mark.asyncio
285+
async def test_invalid_event_emits_structlog_logs(self):
286+
shark = SocketShark(TEST_CONFIG)
287+
session = MockClient(shark).session
288+
with capture_logs() as structlog_logs:
289+
await session.on_service_event({'invalid': 'event'})
290+
assert structlog_logs == [
291+
{
292+
'log_level': 'warning',
293+
'event': 'invalid service event',
294+
'data': {'invalid': 'event'},
295+
'pid': mock.ANY,
296+
'session': mock.ANY,
297+
},
298+
]
299+
300+
@pytest.mark.asyncio
301+
async def test_invalid_published_at_is_handled_gracefully(self):
302+
shark = SocketShark(TEST_CONFIG)
303+
await shark.prepare()
304+
client = MockClient(shark)
305+
session = client.session
306+
307+
await session.on_client_event(
308+
{
309+
'event': 'subscribe',
310+
'subscription': 'simple.topic',
311+
}
312+
)
313+
assert client.log.pop() == {
314+
'event': 'subscribe',
315+
'subscription': 'simple.topic',
316+
'status': 'ok',
317+
}
318+
319+
with capture_logs() as structlog_logs:
320+
await session.on_service_event(
321+
{
322+
'subscription': 'simple.topic',
323+
'data': {'foo': 'bar'},
324+
'published_at': 'not-a-date',
325+
}
326+
)
327+
328+
assert structlog_logs == [
329+
{
330+
'log_level': 'warning',
331+
'event': 'invalid published_at format',
332+
'published_at': 'not-a-date',
333+
'session': mock.ANY,
334+
'pid': mock.ANY,
335+
},
336+
# The message is still expected to be sent. An invalid
337+
# `published_at` should not break the entire end-to-end messaging
338+
# flow .
339+
{
340+
'log_level': 'debug',
341+
'event': 'client send',
342+
'data': {
343+
'subscription': 'simple.topic',
344+
'event': 'message',
345+
'data': {'foo': 'bar'},
346+
'sent_at': mock.ANY,
347+
},
348+
'pid': mock.ANY,
349+
'session': mock.ANY,
350+
},
351+
]
352+
353+
await shark.shutdown()
354+
289355
@pytest.mark.asyncio
290356
async def test_auth_ticket(self):
291357
"""
@@ -299,14 +365,14 @@ async def test_auth_ticket(self):
299365
assert client.log.pop() == {
300366
'status': 'error',
301367
'event': 'auth',
302-
'error': c.ERR_NEEDS_TICKET,
368+
'error': 'Must specify ticket.',
303369
}
304370

305371
await session.on_client_event({'event': 'auth', 'method': 'ticket'})
306372
assert client.log.pop() == {
307373
'status': 'error',
308374
'event': 'auth',
309-
'error': c.ERR_NEEDS_TICKET,
375+
'error': 'Must specify ticket.',
310376
}
311377

312378
with aioresponses() as mock_responses:
@@ -321,7 +387,7 @@ async def test_auth_ticket(self):
321387
assert client.log.pop() == {
322388
'status': 'error',
323389
'event': 'auth',
324-
'error': c.ERR_SERVICE_UNAVAILABLE,
390+
'error': 'Service unavailable.',
325391
}
326392

327393
with aioresponses() as mock_responses:
@@ -359,7 +425,7 @@ async def test_auth_ticket(self):
359425
assert client.log.pop() == {
360426
'status': 'error',
361427
'event': 'auth',
362-
'error': c.ERR_AUTH_FAILED,
428+
'error': 'Authentication failed.',
363429
}
364430

365431
assert session.auth_info == {}
@@ -412,7 +478,7 @@ async def test_subscription_invalid(self):
412478
assert client.log.pop() == {
413479
'event': 'subscribe',
414480
'status': 'error',
415-
'error': c.ERR_INVALID_SUBSCRIPTION_FORMAT,
481+
'error': 'Invalid subscription format.',
416482
}
417483

418484
await session.on_client_event(
@@ -422,7 +488,7 @@ async def test_subscription_invalid(self):
422488
'event': 'subscribe',
423489
'subscription': 'invalid',
424490
'status': 'error',
425-
'error': c.ERR_INVALID_SUBSCRIPTION_FORMAT,
491+
'error': 'Invalid subscription format.',
426492
}
427493

428494
await session.on_client_event(
@@ -432,7 +498,7 @@ async def test_subscription_invalid(self):
432498
'event': 'subscribe',
433499
'subscription': 'invalid.topic',
434500
'status': 'error',
435-
'error': c.ERR_INVALID_SERVICE,
501+
'error': 'Invalid service.',
436502
}
437503

438504
assert not client.log
@@ -456,7 +522,7 @@ async def test_subscription_needs_auth(self):
456522
'event': 'subscribe',
457523
'subscription': 'empty.topic',
458524
'status': 'error',
459-
'error': c.ERR_AUTH_REQUIRED,
525+
'error': 'Authentication required.',
460526
}
461527

462528
assert not client.log
@@ -490,7 +556,7 @@ async def test_subscription_validation(self):
490556
'event': 'subscribe',
491557
'subscription': 'simple.topic',
492558
'status': 'error',
493-
'error': c.ERR_ALREADY_SUBSCRIBED,
559+
'error': 'Already subscribed.',
494560
}
495561

496562
await session.on_client_event(
@@ -504,7 +570,7 @@ async def test_subscription_validation(self):
504570
'status': 'error',
505571
'subscription': 'simple.invalid',
506572
'event': 'message',
507-
'error': c.ERR_SUBSCRIPTION_NOT_FOUND,
573+
'error': 'Subscription does not exist.',
508574
}
509575

510576
await session.on_client_event(
@@ -517,7 +583,7 @@ async def test_subscription_validation(self):
517583
'event': 'unsubscribe',
518584
'subscription': 'simple.invalid',
519585
'status': 'error',
520-
'error': c.ERR_SUBSCRIPTION_NOT_FOUND,
586+
'error': 'Subscription does not exist.',
521587
}
522588

523589
await shark.shutdown()
@@ -679,7 +745,7 @@ async def test_subscription_auth(self):
679745
'event': 'subscribe',
680746
'subscription': 'simple_auth.topic',
681747
'status': 'error',
682-
'error': c.ERR_AUTH_REQUIRED,
748+
'error': 'Authentication required.',
683749
}
684750

685751
await self._auth_session(session)
@@ -775,7 +841,7 @@ async def test_subscription_authorizer(self):
775841
'event': 'subscribe',
776842
'subscription': 'authorizer.topic',
777843
'status': 'error',
778-
'error': c.ERR_AUTH_REQUIRED,
844+
'error': 'Authentication required.',
779845
}
780846

781847
await self._auth_session(session)
@@ -792,7 +858,7 @@ async def test_subscription_authorizer(self):
792858
'event': 'subscribe',
793859
'subscription': 'authorizer.topic',
794860
'status': 'error',
795-
'error': c.ERR_SERVICE_UNAVAILABLE,
861+
'error': 'Service unavailable.',
796862
}
797863

798864
with aioresponses() as mock_responses:
@@ -831,7 +897,7 @@ async def test_subscription_authorizer(self):
831897
'event': 'subscribe',
832898
'subscription': 'authorizer.topic',
833899
'status': 'error',
834-
'error': c.ERR_UNAUTHORIZED,
900+
'error': 'Unauthorized.',
835901
}
836902

837903
await session.on_client_event(
@@ -1023,7 +1089,7 @@ async def test_subscription_periodic_authorizer(self):
10231089
assert client.log.pop(0) == {
10241090
'event': 'unsubscribe',
10251091
'subscription': 'periodic_authorizer.topic',
1026-
'error': c.ERR_UNAUTHORIZED,
1092+
'error': 'Unauthorized.',
10271093
}
10281094

10291095
assert client.log.pop(0) == {
@@ -1182,7 +1248,7 @@ async def test_subscription_complex(self):
11821248
'event': 'subscribe',
11831249
'subscription': 'complex.topic',
11841250
'status': 'error',
1185-
'error': c.ERR_UNHANDLED_EXCEPTION,
1251+
'error': 'Unhandled exception.',
11861252
}
11871253

11881254
mock_responses.post(conf['authorizer'], payload={'status': 'ok'})
@@ -1433,7 +1499,7 @@ async def test_subscription_complex(self):
14331499
'subscription': 'complex.topic',
14341500
'status': 'error',
14351501
'extra': 'hello',
1436-
'error': c.ERR_UNHANDLED_EXCEPTION,
1502+
'error': 'Unhandled exception.',
14371503
}
14381504

14391505
await session.on_client_event(

0 commit comments

Comments
 (0)