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