Skip to content

Commit 6fe4ea5

Browse files
committed
Lifts aiohttp upper version cap by refactoring a subset of tests to use mocks.
1 parent e7027dc commit 6fe4ea5

File tree

10 files changed

+187
-76
lines changed

10 files changed

+187
-76
lines changed

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2024 The MathWorks, Inc.
1+
# Copyright 2020-2025 The MathWorks, Inc.
22
import os
33
from pathlib import Path
44
from shutil import which
@@ -60,17 +60,17 @@ def run(self):
6060
"pytest-timeout",
6161
"psutil",
6262
"urllib3",
63-
"requests",
6463
"pytest-playwright",
6564
]
6665

6766
INSTALL_REQUIRES = [
68-
"aiohttp>=3.7.4, <=3.10.5",
67+
"aiohttp>=3.7.4",
6968
"aiohttp_session[secure]",
7069
"importlib-metadata",
7170
"importlib-resources",
7271
"psutil",
7372
"watchdog",
73+
"requests",
7474
]
7575

7676
HERE = Path(__file__).parent.resolve()

tests/integration/integration_tests_with_license/test_http_end_points.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2023-2024 The MathWorks, Inc.
1+
# Copyright 2023-2025 The MathWorks, Inc.
22

33
"""
44
Contains integration tests which exercise HTTP endpoints of interest exposed by matlab-proxy-app
@@ -246,24 +246,23 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
246246
# Fixtures
247247
@pytest.fixture
248248
def matlab_proxy_app_fixture(
249-
loop,
249+
event_loop,
250250
):
251251
"""A pytest fixture which yields a real matlab server to be used by tests.
252252
253253
Args:
254-
loop (Event event_loop): The built-in event event_loop provided by pytest.
254+
event_loop (Event loop): The built-in event loop provided by pytest.
255255
256256
Yields:
257257
real_matlab_server : A real matlab web server used by tests.
258258
"""
259259

260260
try:
261-
with RealMATLABServer(loop) as matlab_proxy_app:
261+
with RealMATLABServer(event_loop) as matlab_proxy_app:
262262
yield matlab_proxy_app
263263
except ProcessLookupError as e:
264264
_logger.debug("ProcessLookupError found in matlab proxy app fixture")
265265
_logger.debug(e)
266-
pass
267266

268267

269268
@pytest.fixture
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2025 The MathWorks, Inc.
2+
"""A common fixture that could be used by various test classes to disable authentication"""
3+
4+
import pytest
5+
6+
7+
@pytest.fixture
8+
def patch_authenticate_access_decorator(mocker):
9+
"""
10+
Fixture to patch the authenticate_access decorator for testing purposes.
11+
12+
This fixture mocks the 'authenticate_request' function from the
13+
'token_auth' module to always return True,
14+
effectively bypassing authentication for tests.
15+
"""
16+
return mocker.patch(
17+
"matlab_proxy.util.mwi.token_auth.authenticate_request",
18+
return_value=True,
19+
)

tests/unit/mocks/mock_client.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2025 The MathWorks, Inc.
2+
3+
4+
class MockWebSocketClient:
5+
"""Mock class for testing WebSocket client functionality.
6+
7+
This class simulates a WebSocket client for testing purposes, providing async iteration
8+
over predefined messages and basic client functionality.
9+
10+
Args:
11+
text (str, optional): Text to be returned by text() method. Defaults to None.
12+
status (int, optional): HTTP status code. Defaults to 200.
13+
headers (dict, optional): HTTP headers. Defaults to None.
14+
messages (list, optional): List of messages to be returned during iteration. Defaults to None.
15+
"""
16+
17+
def __init__(
18+
self, text=None, status: int = 200, headers=None, messages=None
19+
) -> None:
20+
self._text = text
21+
self.status = status
22+
self.headers = headers
23+
self.messages = messages or []
24+
self._message_iter = iter(self.messages)
25+
26+
def __aiter__(self):
27+
return self
28+
29+
async def __anext__(self):
30+
try:
31+
return next(self._message_iter)
32+
except StopIteration as exc:
33+
raise StopAsyncIteration from exc
34+
35+
async def text(self):
36+
return self._text
37+
38+
async def __aexit__(self, *args) -> None:
39+
pass
40+
41+
async def __aenter__(self):
42+
return self

tests/unit/test_app.py

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2024 The MathWorks, Inc.
1+
# Copyright 2020-2025 The MathWorks, Inc.
22

33
import asyncio
44
import datetime
@@ -9,13 +9,18 @@
99
from datetime import timedelta, timezone
1010
from http import HTTPStatus
1111

12-
import aiohttp
1312
import pytest
14-
import tests.unit.test_constants as test_constants
13+
from aiohttp import WSMsgType
14+
from aiohttp.web import WebSocketResponse
15+
from multidict import CIMultiDict
1516

17+
import tests.unit.test_constants as test_constants
1618
from matlab_proxy import app, util
19+
from matlab_proxy.app import matlab_view
1720
from matlab_proxy.util.mwi import environment_variables as mwi_env
1821
from matlab_proxy.util.mwi.exceptions import EntitlementError, MatlabInstallError
22+
from tests.unit.fixtures.fixture_auth import patch_authenticate_access_decorator
23+
from tests.unit.mocks.mock_client import MockWebSocketClient
1924

2025

2126
@pytest.mark.parametrize(
@@ -61,7 +66,7 @@ def test_configure_no_proxy_in_env(monkeypatch, no_proxy_user_configuration):
6166
)
6267

6368

64-
def test_create_app(loop):
69+
def test_create_app(event_loop):
6570
"""Test if aiohttp server is being created successfully.
6671
6772
Checks if the aiohttp server is created successfully, routes, startup and cleanup
@@ -75,7 +80,7 @@ def test_create_app(loop):
7580
# Verify app server has a cleanup task
7681
# By default there is 1 for clean up task
7782
assert len(test_server.on_cleanup) > 1
78-
loop.run_until_complete(test_server["state"].stop_server_tasks())
83+
event_loop.run_until_complete(test_server["state"].stop_server_tasks())
7984

8085

8186
def get_email():
@@ -245,9 +250,33 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
245250
self.loop.run_until_complete(self.server.cleanup())
246251

247252

253+
@pytest.fixture
254+
def mock_request(mocker):
255+
"""Creates a mock request with required attributes"""
256+
req = mocker.MagicMock()
257+
req.app = {
258+
"state": mocker.MagicMock(matlab_port=8000),
259+
"settings": {"matlab_protocol": "http", "mwapikey": "test-key"},
260+
}
261+
req.headers = CIMultiDict()
262+
req.cookies = {}
263+
return req
264+
265+
266+
@pytest.fixture(name="mock_websocket_messages")
267+
def mock_messages(mocker):
268+
# Mock WebSocket messages
269+
return [
270+
mocker.MagicMock(type=WSMsgType.TEXT, data="test message"),
271+
mocker.MagicMock(type=WSMsgType.BINARY, data=b"test binary"),
272+
mocker.MagicMock(type=WSMsgType.PING),
273+
mocker.MagicMock(type=WSMsgType.PONG),
274+
]
275+
276+
248277
@pytest.fixture(name="test_server")
249278
def test_server_fixture(
250-
loop,
279+
event_loop,
251280
aiohttp_client,
252281
monkeypatch,
253282
):
@@ -263,7 +292,7 @@ def test_server_fixture(
263292
# Disabling the authentication token mechanism explicitly
264293
monkeypatch.setenv(mwi_env.get_env_name_enable_mwi_auth_token(), "False")
265294
try:
266-
with FakeServer(loop, aiohttp_client) as test_server:
295+
with FakeServer(event_loop, aiohttp_client) as test_server:
267296
yield test_server
268297
except ProcessLookupError:
269298
pass
@@ -350,7 +379,7 @@ async def test_start_matlab_route(test_server):
350379
await __check_for_matlab_status(test_server, "starting")
351380

352381

353-
async def __check_for_matlab_status(test_server, status):
382+
async def __check_for_matlab_status(test_server, status, sleep_interval=0.5):
354383
"""Helper function to check if the status of MATLAB returned by the server is either of the values mentioned in statuses
355384
356385
Args:
@@ -369,7 +398,7 @@ async def __check_for_matlab_status(test_server, status):
369398
break
370399
else:
371400
count += 1
372-
await asyncio.sleep(0.5)
401+
await asyncio.sleep(sleep_interval)
373402
if count > test_constants.FIVE_MAX_TRIES:
374403
raise ConnectionError
375404

@@ -592,19 +621,6 @@ async def test_matlab_proxy_http_post_request(proxy_payload, test_server):
592621
raise ConnectionError
593622

594623

595-
# While acceessing matlab-proxy directly, the web socket request looks like
596-
# {
597-
# "connection": "Upgrade",
598-
# "Upgrade": "websocket",
599-
# }
600-
# whereas while accessing matlab-proxy with nginx as the reverse proxy, the nginx server
601-
# modifies the web socket request to
602-
# {
603-
# "connection": "upgrade",
604-
# "upgrade": "websocket",
605-
# }
606-
607-
608624
async def test_set_licensing_info_put_nlm(test_server):
609625
"""Test to check endpoint : "/set_licensing_info"
610626
@@ -641,35 +657,70 @@ async def test_set_licensing_info_put_invalid_license(test_server):
641657
assert resp.status == HTTPStatus.BAD_REQUEST
642658

643659

660+
# While acceessing matlab-proxy directly, the web socket request looks like
661+
# {
662+
# "connection": "Upgrade",
663+
# "Upgrade": "websocket",
664+
# }
665+
# whereas while accessing matlab-proxy with nginx as the reverse proxy, the nginx server
666+
# modifies the web socket request to
667+
# {
668+
# "connection": "upgrade",
669+
# "upgrade": "websocket",
670+
# }
644671
@pytest.mark.parametrize(
645672
"headers",
646673
[
647-
{
648-
"connection": "Upgrade",
649-
"Upgrade": "websocket",
650-
},
651-
{
652-
"connection": "upgrade",
653-
"upgrade": "websocket",
654-
},
674+
CIMultiDict(
675+
{
676+
"connection": "Upgrade",
677+
"Upgrade": "websocket",
678+
}
679+
),
680+
CIMultiDict(
681+
{
682+
"connection": "upgrade",
683+
"upgrade": "websocket",
684+
}
685+
),
655686
],
656687
ids=["Uppercase header", "Lowercase header"],
657688
)
658-
async def test_matlab_proxy_web_socket(test_server, headers):
659-
"""Test to check if test_server proxies web socket request to fake matlab server
689+
async def test_matlab_view_websocket_success(
690+
mocker,
691+
mock_request,
692+
mock_websocket_messages,
693+
headers,
694+
patch_authenticate_access_decorator,
695+
):
696+
"""Test successful websocket connection and message forwarding"""
660697

661-
Args:
662-
test_server (aiohttp_client): Test Server to send HTTP Requests.
663-
"""
698+
# Configure request for WebSocket
699+
mock_request.headers = headers
700+
mock_request.method = "GET"
701+
mock_request.path_qs = "/test"
664702

665-
await wait_for_matlab_to_be_up(test_server, test_constants.ONE_SECOND_DELAY)
666-
resp = await test_server.ws_connect("/http_ws_request.html/", headers=headers)
667-
text = await resp.receive()
668-
websocket_response_string = (
669-
"Hello world" # This string is set by the web_socket_handler in devel.py
703+
# Mock WebSocket setup
704+
mock_ws_server = mocker.MagicMock(spec=WebSocketResponse)
705+
mocker.patch(
706+
"matlab_proxy.app.aiohttp.web.WebSocketResponse", return_value=mock_ws_server
707+
)
708+
709+
# Mock WebSocket client
710+
mock_ws_client = MockWebSocketClient(messages=mock_websocket_messages)
711+
mocker.patch(
712+
"matlab_proxy.app.aiohttp.ClientSession.ws_connect", return_value=mock_ws_client
670713
)
671-
assert text.type == aiohttp.WSMsgType.TEXT
672-
assert text.data == websocket_response_string
714+
715+
# Execute
716+
result = await matlab_view(mock_request)
717+
718+
# Assertions
719+
assert result == mock_ws_server
720+
assert mock_ws_server.send_str.call_count == 1
721+
assert mock_ws_server.send_bytes.call_count == 1
722+
assert mock_ws_server.ping.call_count == 1
723+
assert mock_ws_server.pong.call_count == 1
673724

674725

675726
async def test_set_licensing_info_put_mhlm(test_server):
@@ -992,7 +1043,7 @@ async def test_set_licensing_mhlm_single_entitlement(
9921043
assert resp_json["licensing"]["entitlementId"] == "Entitlement3"
9931044

9941045
# validate that MATLAB has started correctly
995-
await __check_for_matlab_status(test_server, "up")
1046+
await __check_for_matlab_status(test_server, "up", sleep_interval=2)
9961047

9971048
# test-cleanup: unset licensing
9981049
# without this, we can leave test drool related to cached license file

0 commit comments

Comments
 (0)