11from fastapi .testclient import TestClient
2- from labthings_fastapi .example_things import MyThing
32import pytest
43import labthings_fastapi as lt
54from labthings_fastapi .exceptions import PropertyNotObservableError
65
76
8- @pytest .fixture
9- def my_thing ():
10- return MyThing ()
11-
12-
13- @pytest .fixture
14- def server (my_thing ):
15- server = lt .ThingServer ()
16- server .add_thing (my_thing , "/my_thing" )
17- return server
18-
19-
20- def test_websocket_observeproperty (server ):
21- with TestClient (server .app ) as client :
22- with client .websocket_connect ("/my_thing/ws" ) as ws :
23- ws .send_json (
24- {"messageType" : "addPropertyObservation" , "data" : {"foo" : True }}
25- )
26- test_str = "Abcdef"
27- client .put ("/my_thing/foo" , json = test_str )
28- message = ws .receive_json (mode = "text" )
29- assert message ["data" ]["foo" ] == test_str
30- ws .close (1000 )
7+ class ThingWithProperties (lt .Thing ):
8+ """A Thing with various different properties and actions.
319
10+ This is used by the earlier tests, ensuring properties may
11+ be observed.
12+ """
3213
33- class ThingWithProperties (lt .Thing ):
3414 dataprop : int = lt .property (default = 0 )
3515 non_property : int = 0
3616
3717 @lt .property
3818 def funcprop (self ) -> int :
3919 return 0
4020
41- @lt . property
21+ @funcprop . setter
4222 def set_funcprop (self , val : int ) -> None :
4323 pass
4424
@@ -48,134 +28,163 @@ def increment_dataprop(self):
4828 self .dataprop += 1
4929
5030
51- def test_observing_dataprop (mocker ):
52- """Check `observe_property` is OK on a data property.
53-
54- This doesn't check the observation works, because we don't
55- have an event loop. It just checks the call doesn't raise
56- an error.
57- """
58- thing = ThingWithProperties ()
59- thing .observe_property ("dataprop" , mocker .Mock ())
31+ @pytest .fixture
32+ def thing ():
33+ """Instantiate and return a test Thing."""
34+ return ThingWithProperties ()
6035
6136
62- def test_observing_dataprop_with_ws ():
63- """Observe a data property with a websocket, and check it works."""
37+ @pytest .fixture
38+ def server (thing ):
39+ """Create a server, and add a MyThing test Thing to it."""
6440 server = lt .ThingServer ()
65- server .add_thing (ThingWithProperties (), "/thing" )
66- with TestClient (server .app ) as client :
67- with client .websocket_connect ("/thing/ws" ) as ws :
68- ws .send_json (
69- {"messageType" : "addPropertyObservation" , "data" : {"dataprop" : True }}
70- )
71- client .put ("/thing/dataprop" , json = 1 )
72- message = ws .receive_json (mode = "text" )
73- assert message ["data" ]["dataprop" ] == 1
74- ws .close (1000 )
41+ server .add_thing (thing , "/thing" )
42+ return server
7543
7644
77- def test_observing_funcprop ( mocker ):
78- """Check errors are raised if we observe an unsuitable property."""
79- thing = ThingWithProperties ()
80- with pytest . raises ( PropertyNotObservableError ) :
81- thing . observe_property ( "funcprop" , mocker . Mock ())
45+ @ pytest . fixture
46+ def client ( server ):
47+ """Yield a TestClient connected to the server."""
48+ with TestClient ( server . app ) as client :
49+ yield client
8250
8351
84- def test_observing_funcprop_with_ws ():
85- """Try to observe a functional property with a websocket.
52+ @pytest .fixture
53+ def ws (client ):
54+ """Yield a websocket connection to a server hosting a MyThing().
8655
87- This should fail: functional properties are not observable.
56+ This ensures the websocket is properly closed after the test, and
57+ avoids lots of indent levels.
8858 """
89- server = lt .ThingServer ()
90- server .add_thing (ThingWithProperties (), "/thing" )
91- with TestClient (server .app ) as client :
92- with client .websocket_connect ("/thing/ws" ) as ws :
93- ws .send_json (
94- {"messageType" : "addPropertyObservation" , "data" : {"funcprop" : True }}
95- )
96- message = ws .receive_json (mode = "text" )
97- assert message ["error" ]["title" ] == "Not Observable"
59+ with client .websocket_connect ("/thing/ws" ) as ws :
60+ try :
61+ yield ws
62+ finally :
9863 ws .close (1000 )
64+ pass
65+
9966
67+ def test_observing_dataprop (thing , mocker ):
68+ """Check `observe_property` is OK on a data property.
10069
101- def test_observing_missing_prop ( mocker ):
102- """Check observing a non-existent property raises an error."""
103- thing = ThingWithProperties ()
104- with pytest . raises ( AttributeError ):
105- thing .observe_property ("missing_property " , mocker .Mock ())
70+ This doesn't check the observation works, because we don't
71+ have an event loop. It just checks the call doesn't raise
72+ an error.
73+ """
74+ thing .observe_property ("dataprop " , mocker .Mock ())
10675
10776
108- def test_observing_not_prop (mocker ):
109- """Check observing an attribute that's not a property raises an error."""
110- thing = ThingWithProperties ()
111- with pytest .raises (KeyError ):
112- thing .observe_property ("non_property" , mocker .Mock ())
77+ @pytest .mark .parametrize (
78+ argnames = ["name" , "exception" ],
79+ argvalues = [
80+ ("funcprop" , PropertyNotObservableError ),
81+ ("non_property" , KeyError ),
82+ ("increment_dataprop" , KeyError ),
83+ ("missing" , KeyError ),
84+ ],
85+ )
86+ def test_observing_errors (thing , mocker , name , exception ):
87+ """Check errors are raised if we observe an unsuitable property."""
88+ with pytest .raises (exception ):
89+ thing .observe_property (name , mocker .Mock ())
11390
11491
115- def test_observing_action (mocker ):
92+ def test_observing_dataprop_with_ws (client , ws ):
93+ """Observe a data property with a websocket.
94+
95+ This tests the property's value gets notified when it is
96+ set via PUT requests or via an action.
97+ """
98+ # Observe the property.
99+ ws .send_json (
100+ {
101+ "messageType" : "addPropertyObservation" ,
102+ "data" : {"dataprop" : True },
103+ }
104+ )
105+ for val in [1 , 10 , 0 ]:
106+ # Set the property's value.
107+ client .put ("/thing/dataprop" , json = val )
108+ # Receive the message and check it's as expected.
109+ message = ws .receive_json (mode = "text" )
110+ assert message ["messageType" ] == "propertyStatus"
111+ assert message ["data" ]["dataprop" ] == val
112+ # Increment the value with an action
113+ client .post ("/thing/increment_dataprop" )
114+ message = ws .receive_json (mode = "text" )
115+ assert message ["messageType" ] == "propertyStatus"
116+ assert message ["data" ]["dataprop" ] == 1
117+
118+
119+ @pytest .mark .parametrize (
120+ argnames = ["name" , "title" , "status" ],
121+ argvalues = [
122+ ("funcprop" , "Not Observable" , "403" ),
123+ ("non_property" , "Not Found" , "404" ),
124+ ("increment_dataprop" , "Not Found" , "404" ),
125+ ("missing" , "Not Found" , "404" ),
126+ ],
127+ )
128+ def test_observing_dataprop_error_with_ws (ws , name , title , status ):
129+ """Try to observe a functional/missing property with a websocket.
130+
131+ This should fail: functional properties are not observable.
132+ """
133+ # Observe the property.
134+ ws .send_json (
135+ {
136+ "messageType" : "addPropertyObservation" ,
137+ "data" : {name : True },
138+ }
139+ )
140+ # Receive the message and check for the error.
141+ message = ws .receive_json (mode = "text" )
142+ assert message ["error" ]["title" ] == title
143+ assert message ["error" ]["status" ] == status
144+
145+
146+ def test_observing_action (thing , mocker ):
116147 """Check observing an action is successful."""
117- thing = ThingWithProperties ()
118148 thing .observe_action ("increment_dataprop" , mocker .Mock ())
119149
120150
121- def test_observing_not_action ( mocker ):
151+ def test_observing_action_error ( thing , mocker ):
122152 """Check observing an attribute that's not an action raises an error."""
123- thing = ThingWithProperties ()
124153 with pytest .raises (KeyError ):
125154 thing .observe_action ("non_property" , mocker .Mock ())
126155
127156
128- def test_websocket_observeproperty_counter (server ):
129- with TestClient (server .app ) as client :
130- with client .websocket_connect ("/my_thing/ws" ) as ws :
131- ws .send_json (
132- {"messageType" : "addPropertyObservation" , "data" : {"counter" : True }}
133- )
134- # Trigger the increment_counter action to change the counter value
135- client .post ("/my_thing/increment_counter" )
136-
137- # Receive the message from the WebSocket
138- message = ws .receive_json (mode = "text" )
139- assert (
140- message ["data" ]["counter" ] == 1
141- ) # Expect the counter to be 1 after increment
142- ws .close (1000 )
143- my_thing .counter = 0 # Set counter back to 0
144-
145-
146- def handle_websocket_messages (message ):
147- if (
148- message ["messageType" ] == "actionStatus"
149- and message ["data" ]["status" ] == "completed"
150- ):
151- return True
152- return False
153-
154-
155- def test_websocket_observeaction (server , my_thing ):
156- with TestClient (server .app ) as client :
157- with client .websocket_connect ("/my_thing/ws" ) as ws :
158- ws .send_json (
159- {"messageType" : "addPropertyObservation" , "data" : {"counter" : True }}
160- )
161- ws .send_json (
162- {
163- "messageType" : "addActionObservation" ,
164- "data" : {"slowly_increase_counter" : True },
165- }
166- )
167-
168- # Trigger the slowly_increase_counter action
169- client .post ("/my_thing/slowly_increase_counter" , json = {"delay" : 0 })
170-
171- # Listen for WebSocket messages and check for the completed action
172- action_completed = False
173- while not action_completed :
174- message = ws .receive_json ()
175- print (f"Received message: { message } " )
176-
177- action_completed = handle_websocket_messages (message )
178- if action_completed :
179- assert my_thing .counter == 60
180-
181- my_thing .counter = 0 # Set counter back to 0
157+ def test_observing_action_with_ws (client , ws ):
158+ """Observe an action with a websocket, checking the status changes correctly."""
159+ # Observe the property.
160+ ws .send_json (
161+ {
162+ "messageType" : "addActionObservation" ,
163+ "data" : {"increment_dataprop" : True },
164+ }
165+ )
166+ # Invoke the action (via HTTP)
167+ client .post ("/thing/increment_dataprop" )
168+ # We should see the status go through the expected sequence
169+ for expected_status in ["pending" , "running" , "completed" ]:
170+ message = ws .receive_json (mode = "text" )
171+ assert message ["messageType" ] == "actionStatus"
172+ assert message ["data" ]["status" ] == expected_status
173+
174+
175+ def test_observing_action_error_with_ws (ws ):
176+ """Try to observe something that's not an action, as an action.
177+
178+ This should fail: observeAction should only work on actions.
179+ """
180+ # Observe the property.
181+ ws .send_json (
182+ {
183+ "messageType" : "addActionObservation" ,
184+ "data" : {"non_property" : True },
185+ }
186+ )
187+ # Receive the message and check for the error.
188+ message = ws .receive_json (mode = "text" )
189+ assert message ["error" ]["title" ] == "Not Found"
190+ assert message ["error" ]["status" ] == "404"
0 commit comments