Skip to content

Commit ab7a13f

Browse files
committed
TST: unit tests for authenticated websockets
1 parent a96ed4c commit ab7a13f

3 files changed

Lines changed: 195 additions & 18 deletions

File tree

bluesky_httpserver/authentication.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -372,15 +372,21 @@ def get_current_principal_websocket(
372372
if auth_header.startswith("ApiKey "):
373373
api_key = auth_header[len("ApiKey") :].strip()
374374

375-
return get_current_principal(
376-
request=websocket,
377-
security_scopes=security_scopes,
378-
access_token=access_token,
379-
api_key=api_key,
380-
settings=settings,
381-
authenticators=authenticators,
382-
api_access_manager=api_access_manager,
383-
)
375+
principal = None
376+
try:
377+
principal = get_current_principal(
378+
request=websocket,
379+
security_scopes=security_scopes,
380+
access_token=access_token,
381+
api_key=api_key,
382+
settings=settings,
383+
authenticators=authenticators,
384+
api_access_manager=api_access_manager,
385+
)
386+
except HTTPException as ex:
387+
print(f"WebSocket connection failed: {ex}")
388+
389+
return principal
384390

385391

386392
def create_session(settings, identity_provider, id, scopes):
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import json
2+
import pprint
3+
import threading
4+
import time as ttime
5+
6+
import pytest
7+
from bluesky_queueserver.manager.tests.common import re_manager, re_manager_cmd # noqa F401
8+
from websockets.sync.client import connect
9+
10+
from .conftest import fastapi_server_fs # noqa: F401
11+
from .conftest import (
12+
SERVER_ADDRESS,
13+
SERVER_PORT,
14+
request_to_json,
15+
setup_server_with_config_file,
16+
wait_for_environment_to_be_closed,
17+
wait_for_environment_to_be_created,
18+
)
19+
20+
config_toy_test = """
21+
authentication:
22+
allow_anonymous_access: True
23+
providers:
24+
- provider: toy
25+
authenticator: bluesky_httpserver.authenticators:DictionaryAuthenticator
26+
args:
27+
users_to_passwords:
28+
bob: bob_password
29+
alice: alice_password
30+
cara: cara_password
31+
tom: tom_password
32+
api_access:
33+
policy: bluesky_httpserver.authorization:DictionaryAPIAccessControl
34+
args:
35+
users:
36+
bob:
37+
roles:
38+
- admin
39+
- expert
40+
alice:
41+
roles: advanced
42+
tom:
43+
roles: user
44+
"""
45+
46+
47+
class _ReceiveSystemInfoSocket(threading.Thread):
48+
"""
49+
Catch streaming console output by connecting to /console_output/ws socket and
50+
save messages to the buffer.
51+
"""
52+
53+
def __init__(self, *, endpoint, api_key=None, token=None, **kwargs):
54+
super().__init__(**kwargs)
55+
self.received_data_buffer = []
56+
self._exit = False
57+
self._api_key = api_key
58+
self._token = token
59+
self._endpoint = endpoint
60+
61+
def run(self):
62+
websocket_uri = f"ws://{SERVER_ADDRESS}:{SERVER_PORT}/api{self._endpoint}"
63+
if self._token is not None:
64+
additional_headers = {"Authorization": f"Bearer {self._token}"}
65+
elif self._api_key is not None:
66+
additional_headers = {"Authorization": f"ApiKey {self._api_key}"}
67+
else:
68+
additional_headers = {}
69+
70+
try:
71+
with connect(websocket_uri, additional_headers=additional_headers) as websocket:
72+
while not self._exit:
73+
try:
74+
msg_json = websocket.recv(timeout=0.1, decode=False)
75+
try:
76+
msg = json.loads(msg_json)
77+
self.received_data_buffer.append(msg)
78+
except json.JSONDecodeError:
79+
pass
80+
except TimeoutError:
81+
pass
82+
except Exception as ex:
83+
print(f"Failed to connect to server: {ex}")
84+
85+
def stop(self):
86+
"""
87+
Call this method to stop the thread. Then send a request to the server so that some output
88+
is printed in ``stdout``.
89+
"""
90+
self._exit = True
91+
92+
def __del__(self):
93+
self.stop()
94+
95+
96+
# fmt: off
97+
@pytest.mark.parametrize("ws_auth_type", ["apikey", "token", "none"])
98+
# fmt: on
99+
def test_websocket_auth_01(
100+
tmpdir,
101+
monkeypatch,
102+
re_manager_cmd, # noqa: F811
103+
fastapi_server_fs, # noqa: F811
104+
ws_auth_type,
105+
):
106+
"""
107+
``/auth/apikey`` (GET): basic tests.
108+
"""
109+
110+
# Start RE Manager
111+
params = ["--zmq-publish-console", "ON"]
112+
re_manager_cmd(params)
113+
114+
setup_server_with_config_file(config_file_str=config_toy_test, tmpdir=tmpdir, monkeypatch=monkeypatch)
115+
fastapi_server_fs()
116+
117+
resp1 = request_to_json("post", "/auth/provider/toy/token", login=("bob", "bob_password"))
118+
assert "access_token" in pprint.pformat(resp1)
119+
token = resp1["access_token"]
120+
121+
resp3 = request_to_json(
122+
"post", "/auth/apikey", json={"expires_in": 900, "note": "API key for testing"}, token=token
123+
)
124+
assert "secret" in resp3, pprint.pformat(resp3)
125+
assert "note" in resp3, pprint.pformat(resp3)
126+
assert resp3["note"] == "API key for testing"
127+
assert resp3["scopes"] == ["inherit"]
128+
api_key = resp3["secret"]
129+
130+
endpoint = "/status/ws"
131+
if ws_auth_type == "none":
132+
ws_params = {}
133+
elif ws_auth_type == "apikey":
134+
ws_params = {"api_key": api_key}
135+
elif ws_auth_type == "token":
136+
ws_params = {"token": token}
137+
else:
138+
assert False, f"Unknown authentication type: {ws_auth_type!r}"
139+
140+
rsc = _ReceiveSystemInfoSocket(endpoint=endpoint, **ws_params)
141+
rsc.start()
142+
ttime.sleep(1) # Wait until the client connects to the socket
143+
144+
resp1 = request_to_json("post", "/environment/open", api_key=api_key)
145+
assert resp1["success"] is True, pprint.pformat(resp1)
146+
147+
assert wait_for_environment_to_be_created(timeout=10, api_key=api_key)
148+
149+
resp2b = request_to_json("post", "/environment/close", api_key=api_key)
150+
assert resp2b["success"] is True, pprint.pformat(resp2b)
151+
152+
assert wait_for_environment_to_be_closed(timeout=10, api_key=api_key)
153+
154+
# Wait until capture is complete
155+
ttime.sleep(2)
156+
rsc.stop()
157+
rsc.join()
158+
159+
buffer = rsc.received_data_buffer
160+
if ws_auth_type == "none":
161+
assert len(buffer) == 0
162+
else:
163+
assert len(buffer) > 0
164+
for msg in buffer:
165+
assert "time" in msg, msg
166+
assert isinstance(msg["time"], float), msg
167+
assert "msg" in msg
168+
assert isinstance(msg["msg"], dict)

bluesky_httpserver/tests/test_system_info_socket.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,20 @@ def __init__(self, *, endpoint, api_key=API_KEY_FOR_TESTS, **kwargs):
3636
def run(self):
3737
websocket_uri = f"ws://{SERVER_ADDRESS}:{SERVER_PORT}/api{self._endpoint}"
3838
additional_headers = {"Authorization": f"ApiKey {self._api_key}"}
39-
with connect(websocket_uri, additional_headers=additional_headers) as websocket:
40-
while not self._exit:
41-
try:
42-
msg_json = websocket.recv(timeout=0.1, decode=False)
39+
try:
40+
with connect(websocket_uri, additional_headers=additional_headers) as websocket:
41+
while not self._exit:
4342
try:
44-
msg = json.loads(msg_json)
45-
self.received_data_buffer.append(msg)
46-
except json.JSONDecodeError:
43+
msg_json = websocket.recv(timeout=0.1, decode=False)
44+
try:
45+
msg = json.loads(msg_json)
46+
self.received_data_buffer.append(msg)
47+
except json.JSONDecodeError:
48+
pass
49+
except TimeoutError:
4750
pass
48-
except TimeoutError:
49-
pass
51+
except Exception as ex:
52+
print(f"Failed to connect to server: {ex}")
5053

5154
def stop(self):
5255
"""

0 commit comments

Comments
 (0)