matlab-proxy 0.25.1__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of matlab-proxy might be problematic. Click here for more details.

Files changed (63) hide show
  1. matlab_proxy/app.py +68 -16
  2. matlab_proxy/app_state.py +8 -2
  3. matlab_proxy/constants.py +1 -0
  4. matlab_proxy/default_configuration.py +2 -2
  5. matlab_proxy/gui/index.html +1 -1
  6. matlab_proxy/gui/static/js/{index.CZgGkMCD.js → index.BcDShXfH.js} +16 -16
  7. matlab_proxy/settings.py +24 -2
  8. matlab_proxy/util/cookie_jar.py +72 -0
  9. matlab_proxy/util/list_servers.py +2 -2
  10. matlab_proxy/util/mwi/environment_variables.py +15 -0
  11. matlab_proxy/util/mwi/session_name.py +28 -0
  12. matlab_proxy/util/mwi/validators.py +2 -4
  13. {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/METADATA +37 -23
  14. {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/RECORD +29 -56
  15. {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/WHEEL +1 -2
  16. matlab_proxy_manager/README.md +85 -0
  17. matlab_proxy_manager/lib/README.md +53 -0
  18. matlab_proxy_manager/lib/api.py +156 -114
  19. matlab_proxy_manager/storage/README.md +54 -0
  20. matlab_proxy_manager/storage/server.py +5 -2
  21. matlab_proxy_manager/utils/constants.py +2 -1
  22. matlab_proxy_manager/utils/environment_variables.py +6 -1
  23. matlab_proxy_manager/utils/exceptions.py +45 -0
  24. matlab_proxy_manager/utils/helpers.py +2 -2
  25. matlab_proxy_manager/utils/logger.py +4 -1
  26. matlab_proxy_manager/web/README.md +37 -0
  27. matlab_proxy_manager/web/app.py +71 -19
  28. matlab_proxy-0.25.1.dist-info/top_level.txt +0 -3
  29. tests/integration/__init__.py +0 -1
  30. tests/integration/integration_tests_with_license/__init__.py +0 -1
  31. tests/integration/integration_tests_with_license/conftest.py +0 -47
  32. tests/integration/integration_tests_with_license/test_http_end_points.py +0 -397
  33. tests/integration/integration_tests_without_license/__init__.py +0 -1
  34. tests/integration/integration_tests_without_license/conftest.py +0 -116
  35. tests/integration/integration_tests_without_license/test_matlab_is_down_if_unlicensed.py +0 -49
  36. tests/integration/utils/__init__.py +0 -1
  37. tests/integration/utils/integration_tests_utils.py +0 -352
  38. tests/integration/utils/licensing.py +0 -152
  39. tests/unit/__init__.py +0 -1
  40. tests/unit/conftest.py +0 -66
  41. tests/unit/test_app.py +0 -1200
  42. tests/unit/test_app_state.py +0 -1094
  43. tests/unit/test_constants.py +0 -7
  44. tests/unit/test_ddux.py +0 -22
  45. tests/unit/test_devel.py +0 -246
  46. tests/unit/test_non_dev_mode.py +0 -169
  47. tests/unit/test_settings.py +0 -659
  48. tests/unit/util/__init__.py +0 -3
  49. tests/unit/util/mwi/__init__.py +0 -1
  50. tests/unit/util/mwi/embedded_connector/__init__.py +0 -1
  51. tests/unit/util/mwi/embedded_connector/test_helpers.py +0 -29
  52. tests/unit/util/mwi/embedded_connector/test_request.py +0 -64
  53. tests/unit/util/mwi/test_custom_http_headers.py +0 -281
  54. tests/unit/util/mwi/test_download.py +0 -152
  55. tests/unit/util/mwi/test_logger.py +0 -82
  56. tests/unit/util/mwi/test_token_auth.py +0 -303
  57. tests/unit/util/mwi/test_validators.py +0 -364
  58. tests/unit/util/test_mw.py +0 -550
  59. tests/unit/util/test_util.py +0 -221
  60. tests/utils/__init__.py +0 -1
  61. tests/utils/logging_util.py +0 -81
  62. {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/entry_points.txt +0 -0
  63. {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info/licenses}/LICENSE.md +0 -0
tests/unit/test_app.py DELETED
@@ -1,1200 +0,0 @@
1
- # Copyright 2020-2025 The MathWorks, Inc.
2
-
3
- import asyncio
4
- import datetime
5
- import json
6
- import platform
7
- import random
8
- import time
9
- from datetime import timedelta, timezone
10
- from http import HTTPStatus
11
-
12
- import pytest
13
- from aiohttp import WSMsgType
14
- from aiohttp.web import WebSocketResponse
15
- from multidict import CIMultiDict
16
-
17
- import tests.unit.test_constants as test_constants
18
- from matlab_proxy import app, util
19
- from matlab_proxy.app import matlab_view
20
- from matlab_proxy.util.mwi import environment_variables as mwi_env
21
- 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
24
-
25
-
26
- @pytest.mark.parametrize(
27
- "no_proxy_user_configuration",
28
- [
29
- "",
30
- "1234.1234.1234, localhost , 0.0.0.0,1.2.3.4",
31
- "0.0.0.0",
32
- "1234.1234.1234",
33
- " 1234.1234.1234 ",
34
- ],
35
- )
36
- def test_configure_no_proxy_in_env(monkeypatch, no_proxy_user_configuration):
37
- """Tests the behavior of the configure_no_proxy_in_env function
38
-
39
- Args:
40
- monkeypatch (environment): MonkeyPatches the environment to mimic possible user environment settings
41
- """
42
- no_proxy_user_configuration_set = set(
43
- [val.lstrip().rstrip() for val in no_proxy_user_configuration.split(",")]
44
- )
45
- # Update the environment to simulate user environment
46
- monkeypatch.setenv("no_proxy", no_proxy_user_configuration)
47
-
48
- # This function will modify the environment variables to include 0.0.0.0, localhost & 127.0.0.1
49
- app.configure_no_proxy_in_env()
50
-
51
- import os
52
-
53
- modified_no_proxy_env = os.environ.get("no_proxy")
54
-
55
- # Convert to set to compare, as list generated need not be ordered
56
- modified_no_proxy_env_set = set(
57
- [val.lstrip().rstrip() for val in modified_no_proxy_env.split(",")]
58
- )
59
-
60
- expected_no_proxy_configuration_set = {"0.0.0.0", "localhost", "127.0.0.1"}
61
-
62
- # We expect the modified set of values to include the localhost configurations
63
- # along with whatever else the user had set with no duplicates.
64
- assert modified_no_proxy_env_set == no_proxy_user_configuration_set.union(
65
- expected_no_proxy_configuration_set
66
- )
67
-
68
-
69
- def test_create_app(event_loop):
70
- """Test if aiohttp server is being created successfully.
71
-
72
- Checks if the aiohttp server is created successfully, routes, startup and cleanup
73
- tasks are added.
74
- """
75
- test_server = app.create_app()
76
-
77
- # Verify router is configured with some routes
78
- assert test_server.router._resources is not None
79
-
80
- # Verify app server has a cleanup task
81
- # By default there is 1 for clean up task
82
- assert len(test_server.on_cleanup) > 1
83
- event_loop.run_until_complete(test_server["state"].stop_server_tasks())
84
-
85
-
86
- def get_email():
87
- """Returns a placeholder email
88
-
89
- Returns:
90
- String: A placeholder email as a string.
91
- """
92
- return "abc@mathworks.com"
93
-
94
-
95
- def get_connection_string():
96
- """Returns a placeholder nlm connection string
97
-
98
- Returns:
99
- String : A placeholder nlm connection string
100
- """
101
- return "nlm@localhost.com"
102
-
103
-
104
- async def wait_for_matlab_to_be_up(test_server, sleep_seconds):
105
- """Checks at max five times for the MATLAB status to be up and throws ConnectionError
106
- if MATLAB status is not up.
107
-
108
- This function mitigates the scenario where the tests may try to send the request
109
- to the test server and the MATLAB status is not up yet which may cause the test to fail
110
- unexpectedly.
111
-
112
- Use this function if the test intends to wait for the matlab status to be up before
113
- sending any requests.
114
-
115
- Args:
116
- test_server (aiohttp_client) : A aiohttp_client server to send HTTP GET request.
117
- sleep_seconds : Seconds to be sent to the asyncio.sleep method
118
- """
119
-
120
- count = 0
121
- while True:
122
- resp = await test_server.get("/get_status")
123
- assert resp.status == HTTPStatus.OK
124
-
125
- resp_json = json.loads(await resp.text())
126
-
127
- if resp_json["matlab"]["status"] == "up":
128
- break
129
- else:
130
- count += 1
131
- await asyncio.sleep(sleep_seconds)
132
- if count > test_constants.FIVE_MAX_TRIES:
133
- raise ConnectionError
134
-
135
-
136
- @pytest.fixture(
137
- name="licensing_data",
138
- params=[
139
- {"input": None, "expected": None},
140
- {
141
- "input": {"type": "mhlm", "email_addr": get_email()},
142
- "expected": {
143
- "type": "mhlm",
144
- "emailAddress": get_email(),
145
- "entitlements": [],
146
- "entitlementId": None,
147
- },
148
- },
149
- {
150
- "input": {"type": "nlm", "conn_str": get_connection_string()},
151
- "expected": {"type": "nlm", "connectionString": get_connection_string()},
152
- },
153
- {
154
- "input": {"type": "existing_license"},
155
- "expected": {"type": "existing_license"},
156
- },
157
- ],
158
- ids=[
159
- "No Licensing info supplied",
160
- "Licensing type is mhlm",
161
- "Licensing type is nlm",
162
- "Licensing type is existing_license",
163
- ],
164
- )
165
- def licensing_info_fixture(request):
166
- """A pytest fixture which returns licensing_data
167
-
168
- A parameterized pytest fixture which returns a licensing_data dict.
169
- licensing_data of three types:
170
- None : No licensing
171
- MHLM : Matlab Hosted License Manager
172
- NLM : Network License Manager.
173
-
174
-
175
- Args:
176
- request : A built-in pytest fixture
177
-
178
- Returns:
179
- Array : Containing expected and actual licensing data.
180
- """
181
- return request.param
182
-
183
-
184
- def test_marshal_licensing_info(licensing_data):
185
- """Test app.marshal_licensing_info method works correctly
186
-
187
- This test checks if app.marshal_licensing_info returns correct licensing data.
188
- Test checks for 3 cases:
189
- 1) No Licensing Provided
190
- 2) MHLM type Licensing
191
- 3) NLM type licensing
192
-
193
- Args:
194
- licensing_data (Array): An array containing actual and expected licensing data to assert.
195
- """
196
-
197
- actual_licensing_info = licensing_data["input"]
198
- expected_licensing_info = licensing_data["expected"]
199
-
200
- assert app.marshal_licensing_info(actual_licensing_info) == expected_licensing_info
201
-
202
-
203
- @pytest.mark.parametrize(
204
- "actual_error, expected_error",
205
- [
206
- (None, None),
207
- (
208
- MatlabInstallError("'matlab' executable not found in PATH"),
209
- {
210
- "message": "'matlab' executable not found in PATH",
211
- "logs": None,
212
- "type": MatlabInstallError.__name__,
213
- },
214
- ),
215
- ],
216
- ids=["No error", "Raise Matlab Install Error"],
217
- )
218
- def test_marshal_error(actual_error, expected_error):
219
- """Test if marshal_error returns an expected Dict when an error is raised
220
-
221
- Upon raising MatlabInstallError, checks if the the relevant information is returned as a
222
- Dict.
223
-
224
- Args:
225
- actual_error (Exception): An instance of Exception class
226
- expected_error (Dict): A python Dict containing information on the type of Exception
227
- """
228
- assert app.marshal_error(actual_error) == expected_error
229
-
230
-
231
- class FakeServer:
232
- """Context Manager class which returns a web server wrapped in aiohttp_client pytest fixture
233
- for testing.
234
-
235
- The server setup and startup does not need to mimic the way it is being done in main() method in app.py.
236
- Setting up the server in the context of Pytest.
237
- """
238
-
239
- def __init__(self, loop, aiohttp_client):
240
- self.loop = loop
241
- self.aiohttp_client = aiohttp_client
242
-
243
- def __enter__(self):
244
- server = app.create_app()
245
- self.server = app.configure_and_start(server)
246
- return self.loop.run_until_complete(self.aiohttp_client(self.server))
247
-
248
- def __exit__(self, exc_type, exc_value, exc_traceback):
249
- self.loop.run_until_complete(self.server.shutdown())
250
- self.loop.run_until_complete(self.server.cleanup())
251
-
252
-
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
-
277
- @pytest.fixture(name="test_server")
278
- def test_server_fixture(
279
- event_loop,
280
- aiohttp_client,
281
- monkeypatch,
282
- ):
283
- """A pytest fixture which yields a test server to be used by tests.
284
-
285
- Args:
286
- loop (Event loop): The built-in event loop provided by pytest.
287
- aiohttp_client (aiohttp_client): Built-in pytest fixture used as a wrapper to the aiohttp web server.
288
-
289
- Yields:
290
- aiohttp_client : A aiohttp_client server used by tests.
291
- """
292
- # Disabling the authentication token mechanism explicitly
293
- monkeypatch.setenv(mwi_env.get_env_name_enable_mwi_auth_token(), "False")
294
- try:
295
- with FakeServer(event_loop, aiohttp_client) as test_server:
296
- yield test_server
297
- except ProcessLookupError:
298
- pass
299
- finally:
300
- # Cleaning up the env variable related to auth token
301
- monkeypatch.delenv(
302
- mwi_env.get_env_name_enable_mwi_auth_token(), raising="False"
303
- )
304
-
305
-
306
- async def test_get_status_route(test_server):
307
- """Test to check endpoint : "/get_status"
308
-
309
- Args:
310
- test_server (aiohttp_client): A aiohttp_client server for sending GET request.
311
- """
312
-
313
- resp = await test_server.get("/get_status")
314
- assert resp.status == HTTPStatus.OK
315
-
316
-
317
- async def test_clear_client_id_route(test_server):
318
- """Test to check endpoint: "/clear_client_id"
319
-
320
- Args:
321
- test_server (aiohttp_client): A aiohttp_client server for sending POST request.
322
- """
323
-
324
- state = test_server.server.app["state"]
325
- state.active_client = "mock_client_id"
326
- resp = await test_server.post("/clear_client_id")
327
- assert resp.status == HTTPStatus.OK
328
- assert state.active_client is None
329
-
330
-
331
- async def test_get_env_config(test_server):
332
- """Test to check endpoint : "/get_env_config"
333
-
334
- Args:
335
- test_server (aiohttp_client): A aiohttp_client server for sending GET request.
336
- """
337
- expected_json_structure = {
338
- "authentication": {"enabled": False, "status": False},
339
- "matlab": {
340
- "status": "up",
341
- "version": "R2023a",
342
- "supportedVersions": ["R2020b", "R2023a"],
343
- },
344
- "doc_url": "foo",
345
- "extension_name": "bar",
346
- "extension_name_short_description": "foobar",
347
- "should_show_shutdown_button": True,
348
- "isConcurrencyEnabled": "foobar",
349
- "idleTimeoutDuration": 100,
350
- }
351
- resp = await test_server.get("/get_env_config")
352
- assert resp.status == HTTPStatus.OK
353
-
354
- text = await resp.json()
355
- assert text is not None
356
- assert set(expected_json_structure.keys()) == set(text.keys())
357
-
358
-
359
- async def test_start_matlab_route(test_server):
360
- """Test to check endpoint : "/start_matlab"
361
-
362
- Test waits for matlab status to be "up" before sending the GET request to start matlab
363
- Checks whether matlab restarts.
364
-
365
- Args:
366
- test_server (aiohttp_client): A aiohttp_client server to send GET request to.
367
- """
368
- # Waiting for the matlab process to start up.
369
- await wait_for_matlab_to_be_up(
370
- test_server, test_constants.CHECK_MATLAB_STATUS_INTERVAL
371
- )
372
-
373
- # Send get request to end point
374
- await test_server.put("/start_matlab")
375
-
376
- # Check if Matlab restarted successfully
377
- await __check_for_matlab_status(test_server, "starting")
378
-
379
-
380
- async def __check_for_matlab_status(test_server, status, sleep_interval=0.5):
381
- """Helper function to check if the status of MATLAB returned by the server is either of the values mentioned in statuses
382
-
383
- Args:
384
- test_server (aiohttp_client): A aiohttp_client server to send HTTP DELETE request.
385
- statuses ([str]): Possible MATLAB statuses.
386
-
387
- Raises:
388
- ConnectionError: Exception raised if the test_server is not reachable.
389
- """
390
- count = 0
391
- while True:
392
- resp = await test_server.get("/get_status")
393
- assert resp.status == HTTPStatus.OK
394
- resp_json = json.loads(await resp.text())
395
- if resp_json["matlab"]["status"] == status:
396
- break
397
- else:
398
- count += 1
399
- await asyncio.sleep(sleep_interval)
400
- if count > test_constants.FIVE_MAX_TRIES:
401
- raise ConnectionError
402
-
403
-
404
- async def test_stop_matlab_route(test_server):
405
- """Test to check endpoint : "/stop_matlab"
406
-
407
- Sends HTTP DELETE request to stop matlab and checks if matlab status is down.
408
-
409
- Args:
410
- test_server (aiohttp_client): A aiohttp_client server to send HTTP DELETE request.
411
- """
412
- # Arrange
413
- # Nothing to arrange
414
-
415
- # Act
416
- resp = await test_server.delete("/stop_matlab")
417
- assert resp.status == HTTPStatus.OK
418
-
419
- # Assert
420
- # Check if Matlab restarted successfully
421
- await __check_for_matlab_status(test_server, "stopping")
422
-
423
-
424
- async def test_root_redirect(test_server):
425
- """Test to check endpoint : "/"
426
-
427
- Should throw a 404 error. This will look for index.html in root directory of the project
428
- (In non-dev mode, root directory is the package)
429
- This file will not be available in the expected location in dev mode.
430
-
431
- Args:
432
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
433
-
434
- """
435
- count = 0
436
- while True:
437
- resp = await test_server.get("/")
438
- if resp.status == HTTPStatus.SERVICE_UNAVAILABLE:
439
- time.sleep(test_constants.ONE_SECOND_DELAY)
440
- count += 1
441
- else:
442
- assert resp.status == HTTPStatus.NOT_FOUND
443
- break
444
-
445
- if count > test_constants.FIVE_MAX_TRIES:
446
- raise ConnectionError
447
-
448
-
449
- @pytest.fixture(name="proxy_payload")
450
- def proxy_payload_fixture():
451
- """Pytest fixture which returns a Dict representing the payload.
452
-
453
- Returns:
454
- Dict: A Dict representing the payload for HTTP request.
455
- """
456
- payload = {"messages": {"ClientType": [{"properties": {"TYPE": "jsd"}}]}}
457
-
458
- return payload
459
-
460
-
461
- async def test_matlab_proxy_404(proxy_payload, test_server):
462
- """Test to check if test_server is able to proxy HTTP request to fake matlab server
463
- for a non-existing file. Should return 404 status code in response
464
-
465
- Args:
466
- proxy_payload (Dict): Pytest fixture which returns a Dict.
467
- test_server (aiohttp_client): Test server to send HTTP requests.
468
- """
469
-
470
- headers = {"content-type": "application/json"}
471
-
472
- # Request a non-existing html file.
473
- # Request gets proxied to app.matlab_view() which should raise HTTPNotFound() exception ie. return HTTP status code 404
474
-
475
- count = 0
476
- while True:
477
- resp = await test_server.post(
478
- "/1234.html", data=json.dumps(proxy_payload), headers=headers
479
- )
480
- if resp.status == HTTPStatus.SERVICE_UNAVAILABLE:
481
- time.sleep(test_constants.ONE_SECOND_DELAY)
482
- count += 1
483
- else:
484
- assert resp.status == HTTPStatus.NOT_FOUND
485
- break
486
-
487
- if count > test_constants.FIVE_MAX_TRIES:
488
- raise ConnectionError
489
-
490
-
491
- async def test_matlab_proxy_http_get_request(proxy_payload, test_server):
492
- """Test to check if test_server proxies a HTTP request to fake matlab server and returns
493
- the response back
494
-
495
- Args:
496
- proxy_payload (Dict): Pytest fixture which returns a Dict representing payload for the HTTP request
497
- test_server (aiohttp_client): Test server to send HTTP requests.
498
-
499
- Raises:
500
- ConnectionError: If fake matlab server is not reachable from the test server, raises ConnectionError
501
- """
502
-
503
- max_tries = 5
504
- count = 0
505
-
506
- while True:
507
- resp = await test_server.get(
508
- "/http_get_request.html", data=json.dumps(proxy_payload)
509
- )
510
-
511
- if resp.status in (HTTPStatus.NOT_FOUND, HTTPStatus.SERVICE_UNAVAILABLE):
512
- time.sleep(1)
513
- count += 1
514
-
515
- else:
516
- resp_body = await resp.text()
517
- assert json.dumps(proxy_payload) == resp_body
518
- break
519
-
520
- if count > max_tries:
521
- raise ConnectionError
522
-
523
-
524
- async def test_matlab_proxy_http_put_request(proxy_payload, test_server):
525
- """Test to check if test_server proxies a HTTP request to fake matlab server and returns
526
- the response back
527
-
528
- Args:
529
- proxy_payload (Dict): Pytest fixture which returns a Dict representing payload for the HTTP request
530
- test_server (aiohttp_client): Test server to send HTTP requests.
531
-
532
- Raises:
533
- ConnectionError: If fake matlab server is not reachable from the test server, raises ConnectionError
534
- """
535
-
536
- max_tries = 5
537
- count = 0
538
-
539
- while True:
540
- resp = await test_server.put(
541
- "/http_put_request.html", data=json.dumps(proxy_payload)
542
- )
543
-
544
- if resp.status in (HTTPStatus.NOT_FOUND, HTTPStatus.SERVICE_UNAVAILABLE):
545
- time.sleep(1)
546
- count += 1
547
-
548
- else:
549
- resp_body = await resp.text()
550
- assert json.dumps(proxy_payload) == resp_body
551
- break
552
-
553
- if count > max_tries:
554
- raise ConnectionError
555
-
556
-
557
- async def test_matlab_proxy_http_delete_request(proxy_payload, test_server):
558
- """Test to check if test_server proxies a HTTP request to fake matlab server and returns
559
- the response back
560
-
561
- Args:
562
- proxy_payload (Dict): Pytest fixture which returns a Dict representing payload for the HTTP request
563
- test_server (aiohttp_client): Test server to send HTTP requests.
564
-
565
- Raises:
566
- ConnectionError: If fake matlab server is not reachable from the test server, raises ConnectionError
567
- """
568
-
569
- max_tries = 5
570
- count = 0
571
-
572
- while True:
573
- resp = await test_server.delete(
574
- "/http_delete_request.html", data=json.dumps(proxy_payload)
575
- )
576
-
577
- if resp.status in (HTTPStatus.NOT_FOUND, HTTPStatus.SERVICE_UNAVAILABLE):
578
- time.sleep(1)
579
- count += 1
580
-
581
- else:
582
- resp_body = await resp.text()
583
- assert json.dumps(proxy_payload) == resp_body
584
- break
585
-
586
- if count > max_tries:
587
- raise ConnectionError
588
-
589
-
590
- async def test_matlab_proxy_http_post_request(proxy_payload, test_server):
591
- """Test to check if test_server proxies http post request to fake matlab server.
592
- Checks if payload is being modified before proxying.
593
- Args:
594
- proxy_payload (Dict): Pytest fixture which returns a Dict representing payload for the HTTP Request
595
- test_server (aiohttp_client): Test server to send HTTP requests
596
-
597
- Raises:
598
- ConnectionError: If unable to proxy to fake matlab server raise Connection error
599
- """
600
- max_tries = 5
601
- count = 0
602
-
603
- while True:
604
- resp = await test_server.post(
605
- "/messageservice/json/secure",
606
- data=json.dumps(proxy_payload),
607
- )
608
-
609
- if resp.status in (HTTPStatus.NOT_FOUND, HTTPStatus.SERVICE_UNAVAILABLE):
610
- time.sleep(1)
611
- count += 1
612
-
613
- else:
614
- resp_json = await resp.json()
615
- assert set(resp_json.keys()).issubset(proxy_payload.keys())
616
- break
617
-
618
- if count > max_tries:
619
- raise ConnectionError
620
-
621
-
622
- async def test_set_licensing_info_put_nlm(test_server):
623
- """Test to check endpoint : "/set_licensing_info"
624
-
625
- Test which sends HTTP PUT request with NLM licensing information.
626
- Args:
627
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
628
- """
629
-
630
- data = {
631
- "type": "nlm",
632
- "status": "starting",
633
- "version": "R2020b",
634
- "connectionString": "123@nlm",
635
- }
636
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
637
- assert resp.status == HTTPStatus.OK
638
-
639
-
640
- async def test_set_licensing_info_put_invalid_license(test_server):
641
- """Test to check endpoint : "/set_licensing_info"
642
-
643
- Test which sends HTTP PUT request with INVALID licensing information type.
644
- Args:
645
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
646
- """
647
-
648
- data = {
649
- "type": "INVALID_TYPE",
650
- "status": "starting",
651
- "version": "R2020b",
652
- "connectionString": "123@nlm",
653
- }
654
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
655
- assert resp.status == HTTPStatus.BAD_REQUEST
656
-
657
-
658
- # While acceessing matlab-proxy directly, the web socket request looks like
659
- # {
660
- # "connection": "Upgrade",
661
- # "Upgrade": "websocket",
662
- # }
663
- # whereas while accessing matlab-proxy with nginx as the reverse proxy, the nginx server
664
- # modifies the web socket request to
665
- # {
666
- # "connection": "upgrade",
667
- # "upgrade": "websocket",
668
- # }
669
- @pytest.mark.parametrize(
670
- "headers",
671
- [
672
- CIMultiDict(
673
- {
674
- "connection": "Upgrade",
675
- "Upgrade": "websocket",
676
- }
677
- ),
678
- CIMultiDict(
679
- {
680
- "connection": "upgrade",
681
- "upgrade": "websocket",
682
- }
683
- ),
684
- ],
685
- ids=["Uppercase header", "Lowercase header"],
686
- )
687
- async def test_matlab_view_websocket_success(
688
- mocker,
689
- mock_request,
690
- mock_websocket_messages,
691
- headers,
692
- patch_authenticate_access_decorator,
693
- ):
694
- """Test successful websocket connection and message forwarding"""
695
-
696
- # Configure request for WebSocket
697
- mock_request.headers = headers
698
- mock_request.method = "GET"
699
- mock_request.path_qs = "/test"
700
-
701
- # Mock WebSocket setup
702
- mock_ws_server = mocker.MagicMock(spec=WebSocketResponse)
703
- mocker.patch(
704
- "matlab_proxy.app.aiohttp.web.WebSocketResponse", return_value=mock_ws_server
705
- )
706
-
707
- # Mock WebSocket client
708
- mock_ws_client = MockWebSocketClient(messages=mock_websocket_messages)
709
- mocker.patch(
710
- "matlab_proxy.app.aiohttp.ClientSession.ws_connect", return_value=mock_ws_client
711
- )
712
-
713
- # Execute
714
- result = await matlab_view(mock_request)
715
-
716
- # Assertions
717
- assert result == mock_ws_server
718
- assert mock_ws_server.send_str.call_count == 1
719
- assert mock_ws_server.send_bytes.call_count == 1
720
- assert mock_ws_server.ping.call_count == 1
721
- assert mock_ws_server.pong.call_count == 1
722
-
723
-
724
- async def test_set_licensing_info_put_mhlm(test_server):
725
- """Test to check endpoint : "/set_licensing_info"
726
-
727
- Test which sends HTTP PUT request with MHLM licensing information.
728
- Args:
729
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
730
- """
731
- # FIXME: This test is talking to production loginws endpoint and is resulting in an exception.
732
- # TODO: Use mocks to test the mhlm workflows is working as expected
733
- data = {
734
- "type": "mhlm",
735
- "status": "starting",
736
- "version": "R2020b",
737
- "token": "123@nlm",
738
- "emailaddress": "123@nlm",
739
- "sourceId": "123@nlm",
740
- }
741
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
742
- assert resp.status == HTTPStatus.OK
743
-
744
-
745
- async def test_set_licensing_info_put_existing_license(test_server):
746
- """Test to check endpoint : "/set_licensing_info"
747
-
748
- Test which sends HTTP PUT request with local licensing information.
749
- Args:
750
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
751
- """
752
-
753
- data = {"type": "existing_license"}
754
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
755
- assert resp.status == HTTPStatus.OK
756
-
757
-
758
- async def test_set_licensing_info_delete(test_server):
759
- """Test to check endpoint : "/set_licensing_info"
760
-
761
- Test which sends HTTP DELETE request to remove licensing. Checks if licensing is set to None
762
- After request is sent.
763
- Args:
764
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
765
- """
766
-
767
- resp = await test_server.delete("/set_licensing_info")
768
- resp_json = json.loads(await resp.text())
769
- assert resp.status == HTTPStatus.OK and resp_json["licensing"] is None
770
-
771
-
772
- async def test_set_termination_integration_delete(test_server):
773
- """Test to check endpoint : "/shutdown_integration"
774
-
775
- Test which sends HTTP DELETE request to shutdown integration. Checks if integration is shutdown
776
- successfully.
777
- Args:
778
- test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request.
779
- """
780
- # Not awaiting the response here explicitly as the event loop is stopped in the
781
- # handler function.
782
- test_server.delete("/shutdown_integration")
783
-
784
- resp = await test_server.get("/")
785
-
786
- # Assert that the service is unavailable
787
- assert resp.status == 503
788
-
789
-
790
- def test_get_access_url(test_server):
791
- """Should return a url with 127.0.0.1 in test mode
792
-
793
- Args:
794
- test_server (aiohttp.web.Application): Application Server
795
- """
796
-
797
- assert "127.0.0.1" in util.get_access_url(test_server.app)
798
-
799
-
800
- @pytest.fixture(name="non_test_env")
801
- def non_test_env_fixture(monkeypatch):
802
- """Monkeypatches MWI_TEST env var to false
803
-
804
- Args:
805
- monkeypatch (_pytest.monkeypatch.MonkeyPatch): To monkeypatch env vars
806
- """
807
- monkeypatch.setenv(mwi_env.get_env_name_testing(), "false")
808
-
809
-
810
- @pytest.fixture(name="non_default_host_interface")
811
- def non_default_host_interface_fixture(monkeypatch):
812
- """Monkeypatches MWI_TEST env var to false
813
-
814
- Args:
815
- monkeypatch (_pytest.monkeypatch.MonkeyPatch): To monkeypatch env vars
816
- """
817
- monkeypatch.setenv(mwi_env.get_env_name_app_host(), "0.0.0.0")
818
-
819
-
820
- # For pytest fixtures, order of arguments matter.
821
- # First set the default host interface to a non-default value
822
- # Then set MWI_TEST to false and then create an instance of the test_server
823
- # This order will set the test_server with appropriate values.
824
-
825
-
826
- @pytest.mark.skipif(
827
- platform.system() == "Linux" or platform.system() == "Darwin",
828
- reason="Testing the windows access URL",
829
- )
830
- def test_get_access_url_non_dev_windows(
831
- non_default_host_interface, non_test_env, test_server
832
- ):
833
- """Test to check access url to be 127.0.0.1 in non-dev mode on Windows"""
834
- assert "127.0.0.1" in util.get_access_url(test_server.app)
835
-
836
-
837
- @pytest.mark.skipif(
838
- platform.system() == "Windows", reason="Testing the non-Windows access URL"
839
- )
840
- def test_get_access_url_non_dev_posix(
841
- non_default_host_interface, non_test_env, test_server
842
- ):
843
- """Test to check access url to be 0.0.0.0 in non-dev mode on Linux/Darwin"""
844
- assert "0.0.0.0" in util.get_access_url(test_server.app)
845
-
846
-
847
- @pytest.fixture(name="set_licensing_info_mock_fetch_single_entitlement")
848
- def set_licensing_info_mock_fetch_single_entitlement_fixture():
849
- """Fixture that returns a single entitlement
850
-
851
- Returns:
852
- json array: An array consisting of single entitlement information
853
- """
854
- return [
855
- {"id": "Entitlement3", "label": "Label3", "license_number": "License3"},
856
- ]
857
-
858
-
859
- @pytest.fixture(name="set_licensing_info_mock_fetch_multiple_entitlements")
860
- def set_licensing_info_mock_fetch_multiple_entitlements_fixture():
861
- """Fixture that returns multiple entitlements
862
-
863
- Returns:
864
- json array: An array consisting of multiple entitlements
865
- """
866
- return [
867
- {"id": "Entitlement1", "label": "Label1", "license_number": "License1"},
868
- {"id": "Entitlement2", "label": "Label2", "license_number": "License2"},
869
- ]
870
-
871
-
872
- @pytest.fixture(name="set_licensing_info_mock_access_token")
873
- def set_licensing_info_mock_access_token_fixture():
874
- """Pytest fixture that returns a mock token that mimics mw.fetch_access_token() response"""
875
- access_token_string = int("".join([str(random.randint(0, 10)) for _ in range(272)]))
876
- return {
877
- "token": str(access_token_string),
878
- }
879
-
880
-
881
- @pytest.fixture(name="set_licensing_info_mock_expand_token")
882
- def set_licensing_info_mock_expand_token_fixture():
883
- """Pytest fixture which returns a dict
884
-
885
- The return value represents a valid json response when mw.fetch_expand_token function is called.
886
-
887
- Returns:
888
- json data with mimics mw.fetch_expand_token() response
889
- """
890
- now = datetime.datetime.now(timezone.utc)
891
- first_name = "abc"
892
-
893
- json_data = {
894
- "expiry": str((now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%S.%f%z")),
895
- "first_name": first_name,
896
- "last_name": "def",
897
- "display_name": first_name,
898
- "email_addr": "test@test.com",
899
- "user_id": "".join([str(random.randint(0, 10)) for _ in range(13)]),
900
- "profile_id": "".join([str(random.randint(0, 10)) for _ in range(8)]),
901
- }
902
-
903
- return json_data
904
-
905
-
906
- @pytest.fixture(name="set_licensing_info")
907
- async def set_licensing_info_fixture(
908
- mocker,
909
- test_server,
910
- set_licensing_info_mock_expand_token,
911
- set_licensing_info_mock_access_token,
912
- set_licensing_info_mock_fetch_multiple_entitlements,
913
- ):
914
- """Fixture to setup correct licensing state on the server"""
915
- mocker.patch(
916
- "matlab_proxy.app_state.mw.fetch_expand_token",
917
- return_value=set_licensing_info_mock_expand_token,
918
- )
919
-
920
- mocker.patch(
921
- "matlab_proxy.app_state.mw.fetch_access_token",
922
- return_value=set_licensing_info_mock_access_token,
923
- )
924
-
925
- mocker.patch(
926
- "matlab_proxy.app_state.mw.fetch_entitlements",
927
- return_value=set_licensing_info_mock_fetch_multiple_entitlements,
928
- )
929
-
930
- data = {
931
- "type": "mhlm",
932
- "status": "starting",
933
- "version": "R2020b",
934
- "token": "abc@nlm",
935
- "emailAddress": "abc@nlm",
936
- "sourceId": "abc@nlm",
937
- "matlabVersion": "R2023a",
938
- }
939
-
940
- # Waiting for the matlab process to start up.
941
- await wait_for_matlab_to_be_up(test_server, test_constants.ONE_SECOND_DELAY)
942
-
943
- # Set matlab_version to None to check if the version is updated
944
- # after sending a request t o /set_licensing_info endpoint
945
- test_server.server.app["settings"]["matlab_version"] = None
946
-
947
- # Pre-req: stop the matlab that got started during test server startup
948
- resp = await test_server.delete("/stop_matlab")
949
- assert resp.status == HTTPStatus.OK
950
-
951
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
952
- assert resp.status == HTTPStatus.OK
953
-
954
- # Assert whether the matlab_version was updated from None when licensing type is mhlm
955
- assert test_server.server.app["settings"]["matlab_version"] == "R2023a"
956
-
957
- return test_server
958
-
959
-
960
- async def test_set_licensing_mhlm_zero_entitlement(
961
- mocker,
962
- set_licensing_info_mock_expand_token,
963
- set_licensing_info_mock_access_token,
964
- test_server,
965
- ):
966
- # Patching the functions where it is used (and not where it is defined)
967
- mocker.patch(
968
- "matlab_proxy.app_state.mw.fetch_expand_token",
969
- return_value=set_licensing_info_mock_expand_token,
970
- )
971
-
972
- mocker.patch(
973
- "matlab_proxy.app_state.mw.fetch_access_token",
974
- return_value=set_licensing_info_mock_access_token,
975
- )
976
-
977
- mocker.patch(
978
- "matlab_proxy.app_state.mw.fetch_entitlements",
979
- side_effect=EntitlementError(
980
- "Your MathWorks account is not linked to a valid license for MATLAB"
981
- ),
982
- )
983
-
984
- data = {
985
- "type": "mhlm",
986
- "status": "starting",
987
- "version": "R2020b",
988
- "token": "abc@nlm",
989
- "emailaddress": "abc@nlm",
990
- "sourceId": "abc@nlm",
991
- }
992
- # Pre-req: stop the matlab that got started as during test server startup
993
- resp = await test_server.delete("/stop_matlab")
994
- assert resp.status == HTTPStatus.OK
995
-
996
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
997
- assert resp.status == HTTPStatus.OK
998
- resp_json = await resp.json()
999
- expectedError = EntitlementError(message="entitlement error")
1000
- assert resp_json["error"]["type"] == type(expectedError).__name__
1001
-
1002
-
1003
- async def test_set_licensing_mhlm_single_entitlement(
1004
- mocker,
1005
- test_server,
1006
- set_licensing_info_mock_expand_token,
1007
- set_licensing_info_mock_access_token,
1008
- set_licensing_info_mock_fetch_single_entitlement,
1009
- ):
1010
- mocker.patch(
1011
- "matlab_proxy.app_state.mw.fetch_expand_token",
1012
- return_value=set_licensing_info_mock_expand_token,
1013
- )
1014
-
1015
- mocker.patch(
1016
- "matlab_proxy.app_state.mw.fetch_access_token",
1017
- return_value=set_licensing_info_mock_access_token,
1018
- )
1019
-
1020
- mocker.patch(
1021
- "matlab_proxy.app_state.mw.fetch_entitlements",
1022
- return_value=set_licensing_info_mock_fetch_single_entitlement,
1023
- )
1024
-
1025
- data = {
1026
- "type": "mhlm",
1027
- "status": "starting",
1028
- "version": "R2020b",
1029
- "token": "abc@nlm",
1030
- "emailAddress": "abc@nlm",
1031
- "sourceId": "abc@nlm",
1032
- }
1033
- # Pre-req: stop the matlab that got started during test server startup
1034
- resp = await test_server.delete("/stop_matlab")
1035
- assert resp.status == HTTPStatus.OK
1036
-
1037
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
1038
- assert resp.status == HTTPStatus.OK
1039
- resp_json = await resp.json()
1040
- assert len(resp_json["licensing"]["entitlements"]) == 1
1041
- assert resp_json["licensing"]["entitlementId"] == "Entitlement3"
1042
-
1043
- # validate that MATLAB has started correctly
1044
- await __check_for_matlab_status(test_server, "up", sleep_interval=2)
1045
-
1046
- # test-cleanup: unset licensing
1047
- # without this, we can leave test drool related to cached license file
1048
- # which can impact other non-dev workflows
1049
- resp = await test_server.delete("/set_licensing_info")
1050
- assert resp.status == HTTPStatus.OK
1051
-
1052
-
1053
- async def test_set_licensing_mhlm_multi_entitlements(
1054
- mocker,
1055
- test_server,
1056
- set_licensing_info_mock_expand_token,
1057
- set_licensing_info_mock_access_token,
1058
- set_licensing_info_mock_fetch_multiple_entitlements,
1059
- ):
1060
- mocker.patch(
1061
- "matlab_proxy.app_state.mw.fetch_expand_token",
1062
- return_value=set_licensing_info_mock_expand_token,
1063
- )
1064
-
1065
- mocker.patch(
1066
- "matlab_proxy.app_state.mw.fetch_access_token",
1067
- return_value=set_licensing_info_mock_access_token,
1068
- )
1069
-
1070
- mocker.patch(
1071
- "matlab_proxy.app_state.mw.fetch_entitlements",
1072
- return_value=set_licensing_info_mock_fetch_multiple_entitlements,
1073
- )
1074
-
1075
- data = {
1076
- "type": "mhlm",
1077
- "status": "starting",
1078
- "version": "R2020b",
1079
- "token": "abc@nlm",
1080
- "emailaddress": "abc@nlm",
1081
- "sourceId": "abc@nlm",
1082
- }
1083
- # Pre-req: stop the matlab that got started as during test server startup
1084
- resp = await test_server.delete("/stop_matlab")
1085
- assert resp.status == HTTPStatus.OK
1086
-
1087
- resp = await test_server.put("/set_licensing_info", data=json.dumps(data))
1088
- assert resp.status == HTTPStatus.OK
1089
- resp_json = await resp.json()
1090
- assert len(resp_json["licensing"]["entitlements"]) == 2
1091
- assert resp_json["licensing"]["entitlementId"] == None
1092
-
1093
- # MATLAB should not start if there are multiple entitlements and
1094
- # user hasn't selected the license yet
1095
- resp = await test_server.get("/get_status")
1096
- assert resp.status == HTTPStatus.OK
1097
- __check_for_matlab_status(test_server, "down")
1098
-
1099
- # test-cleanup: unset licensing
1100
- resp = await test_server.delete("/set_licensing_info")
1101
- assert resp.status == HTTPStatus.OK
1102
-
1103
-
1104
- async def test_update_entitlement_with_correct_entitlement(set_licensing_info):
1105
- data = {
1106
- "type": "mhlm",
1107
- "entitlement_id": "Entitlement1",
1108
- }
1109
- # This test_server is pre-configured with multiple entitlements on app state but no entitlmentId
1110
- test_server = set_licensing_info
1111
- resp = await test_server.put("/update_entitlement", data=json.dumps(data))
1112
- assert resp.status == HTTPStatus.OK
1113
- resp_json = await resp.json()
1114
- assert resp_json["matlab"]["status"] != "down"
1115
-
1116
- # test-cleanup: unset licensing
1117
- resp = await test_server.delete("/set_licensing_info")
1118
- assert resp.status == HTTPStatus.OK
1119
-
1120
-
1121
- async def test_get_auth_token_route(test_server):
1122
- """Test to check endpoint : "/get_auth_token"
1123
-
1124
- Args:
1125
- test_server (aiohttp_client): A aiohttp_client server for sending GET request.
1126
- """
1127
- resp = await test_server.get("/get_auth_token")
1128
- res_json = await resp.json()
1129
- # Testing the default dev configuration where the auth is disabled
1130
- assert res_json["token"] == None
1131
- assert resp.status == HTTPStatus.OK
1132
-
1133
-
1134
- async def test_check_for_concurrency(test_server):
1135
- """Test to check the response from endpoint : "/get_status" with different query parameters
1136
-
1137
- Test requests the "/get_status" endpoint with different query parameters to check
1138
- how the server responds.
1139
-
1140
- Args:
1141
- test_server (aiohttp_client): A aiohttp_client server to send GET request to.
1142
- """
1143
- # Request server to check if concurrency check is enabled.
1144
-
1145
- env_resp = await test_server.get("/get_env_config")
1146
- assert env_resp.status == HTTPStatus.OK
1147
- env_resp_json = json.loads(await env_resp.text())
1148
- if env_resp_json["isConcurrencyEnabled"]:
1149
- # A normal request should not repond with client id or active status
1150
- status_resp = await test_server.get("/get_status")
1151
- assert status_resp.status == HTTPStatus.OK
1152
- status_resp_json = json.loads(await status_resp.text())
1153
- assert "clientId" not in status_resp_json
1154
- assert "isActiveClient" not in status_resp_json
1155
-
1156
- # When the request comes from the desktop app the server should respond with client id and active status
1157
- status_resp = await test_server.get('/get_status?IS_DESKTOP="true"')
1158
- assert status_resp.status == HTTPStatus.OK
1159
- status_resp_json = json.loads(await status_resp.text())
1160
- assert "clientId" in status_resp_json
1161
- assert "isActiveClient" in status_resp_json
1162
-
1163
- # When the desktop client requests for a session transfer without client id respond with cliend id and active status should be true
1164
- status_resp = await test_server.get(
1165
- '/get_status?IS_DESKTOP="true"&TRANSFER_SESSION="true"'
1166
- )
1167
- assert status_resp.status == HTTPStatus.OK
1168
- status_resp_json = json.loads(await status_resp.text())
1169
- assert "clientId" in status_resp_json
1170
- assert status_resp_json["isActiveClient"] == True
1171
-
1172
- # When transfering the session is requested by a client whihc is not a desktop client it should be ignored
1173
- status_resp = await test_server.get('/get_status?TRANSFER_SESSION="true"')
1174
- assert status_resp.status == HTTPStatus.OK
1175
- status_resp_json = json.loads(await status_resp.text())
1176
- assert "clientId" not in status_resp_json
1177
- assert "isActiveClient" not in status_resp_json
1178
-
1179
- # When the desktop client requests for a session transfer with a client id then respond only with active status
1180
- status_resp = await test_server.get(
1181
- '/get_status?IS_DESKTOP="true"&MWI_CLIENT_ID="foobar"&TRANSFER_SESSION="true"'
1182
- )
1183
- assert status_resp.status == HTTPStatus.OK
1184
- status_resp_json = json.loads(await status_resp.text())
1185
- assert "clientId" not in status_resp_json
1186
- assert status_resp_json["isActiveClient"] == True
1187
- else:
1188
- # When Concurrency check is disabled the response should not contain client id or active status
1189
- status_resp = await test_server.get("/get_status")
1190
- assert status_resp.status == HTTPStatus.OK
1191
- status_resp_json = json.loads(await status_resp.text())
1192
- assert "clientId" not in status_resp_json
1193
- assert "isActiveClient" not in status_resp_json
1194
- status_resp = await test_server.get(
1195
- '/get_status?IS_DESKTOP="true"&MWI_CLIENT_ID="foobar"&TRANSFER_SESSION="true"'
1196
- )
1197
- assert status_resp.status == HTTPStatus.OK
1198
- status_resp_json = json.loads(await status_resp.text())
1199
- assert "clientId" not in status_resp_json
1200
- assert "isActiveClient" not in status_resp_json