1- """Test that Thing Client's can call actions and read properties."""
1+ """Test that Thing Clients can call actions and read properties."""
22
33import re
44
55import pytest
66import labthings_fastapi as lt
77from fastapi .testclient import TestClient
88
9+ from labthings_fastapi .exceptions import ClientPropertyError , FailedToInvokeActionError
10+
911
1012class ThingToTest (lt .Thing ):
1113 """A thing to be tested by using a ThingClient."""
@@ -59,94 +61,188 @@ def throw_value_error(self) -> None:
5961
6062
6163@pytest .fixture
62- def thing_client ():
63- """Yield a test client connected to a ThingServer."""
64+ def thing_client_and_thing ():
65+ """Yield a test client connected to a ThingServer and the Thing itself ."""
6466 server = lt .ThingServer ({"test_thing" : ThingToTest })
6567 with TestClient (server .app ) as client :
66- yield lt .ThingClient .from_url ("/test_thing/" , client = client )
68+ thing_client = lt .ThingClient .from_url ("/test_thing/" , client = client )
69+ thing = server .things ["test_thing" ]
70+ yield thing_client , thing
71+
72+
73+ @pytest .fixture
74+ def thing_client (thing_client_and_thing ):
75+ """Yield a test client connected to a ThingServer."""
76+ return thing_client_and_thing [0 ]
6777
6878
69- def test_reading_and_setting_properties (thing_client ):
79+ def test_reading_and_setting_properties (thing_client_and_thing ):
7080 """Test reading and setting properties."""
81+ thing_client , thing = thing_client_and_thing
82+
83+ # Read the properties from the thing
7184 assert thing_client .int_prop == 1
7285 assert thing_client .float_prop == 0.1
7386 assert thing_client .str_prop == "foo"
7487
88+ # Update via thing client and check they change on the server and in the client
7589 thing_client .int_prop = 2
7690 thing_client .float_prop = 0.2
7791 thing_client .str_prop = "foo2"
7892
93+ # Check the server updated
94+ assert thing .int_prop == 2
95+ assert thing .float_prop == 0.2
96+ assert thing .str_prop == "foo2"
97+ # Check the client updated
7998 assert thing_client .int_prop == 2
8099 assert thing_client .float_prop == 0.2
81100 assert thing_client .str_prop == "foo2"
82101
102+ # Update them on the server side and read them again
103+ thing .int_prop = 3
104+ thing .float_prop = 0.3
105+ thing .str_prop = "foo3"
106+ assert thing_client .int_prop == 3
107+ assert thing_client .float_prop == 0.3
108+ assert thing_client .str_prop == "foo3"
109+
83110 # Set a property that doesn't exist.
84111 err = "Failed to get property foobar: Not Found"
85- with pytest .raises (lt . exceptions . ClientPropertyError , match = err ):
112+ with pytest .raises (ClientPropertyError , match = err ):
86113 thing_client .get_property ("foobar" )
87114
88115 # Set a property with bad data type.
89116 err = (
90- "Failed to get property int_prop: Input should be a valid integer, unable to "
117+ "Failed to set property int_prop: Input should be a valid integer, unable to "
91118 "parse string as an integer"
92119 )
93- with pytest .raises (lt . exceptions . ClientPropertyError , match = err ):
120+ with pytest .raises (ClientPropertyError , match = err ):
94121 thing_client .int_prop = "Bad value!"
95122
96123
97- def test_reading_and_not_setting_read_only_properties (thing_client ):
124+ def test_reading_and_not_setting_read_only_properties (thing_client_and_thing ):
98125 """Test reading read_only properties, but failing to set."""
126+ thing_client , thing = thing_client_and_thing
99127 assert thing_client .int_prop_read_only == 1
100128 assert thing_client .float_prop_read_only == 0.1
101129 assert thing_client .str_prop_read_only == "foo"
102130
103- with pytest .raises (lt . exceptions . ClientPropertyError , match = "Method Not Allowed " ):
131+ with pytest .raises (ClientPropertyError , match = "may not be set " ):
104132 thing_client .int_prop_read_only = 2
105- with pytest .raises (lt . exceptions . ClientPropertyError , match = "Method Not Allowed " ):
133+ with pytest .raises (ClientPropertyError , match = "may not be set " ):
106134 thing_client .float_prop_read_only = 0.2
107- with pytest .raises (lt . exceptions . ClientPropertyError , match = "Method Not Allowed " ):
135+ with pytest .raises (ClientPropertyError , match = "may not be set " ):
108136 thing_client .str_prop_read_only = "foo2"
109137
110138 assert thing_client .int_prop_read_only == 1
111139 assert thing_client .float_prop_read_only == 0.1
112140 assert thing_client .str_prop_read_only == "foo"
113141
114142
115- def test_call_action (thing_client ):
143+ def test_property_descriptor_errors (mocker ):
144+ """This checks that read/write-only properties raise an error when written/read.
145+
146+ Write only properties are not yet supported on the server side, so it's done with a
147+ mocked up Thing Description.
148+ """
149+ thing_description = {
150+ "title" : "Example" ,
151+ "properties" : {
152+ "readwrite" : {
153+ "title" : "test" ,
154+ "writeOnly" : False ,
155+ "readOnly" : False ,
156+ "type" : "integer" ,
157+ "forms" : [],
158+ },
159+ "readonly" : {
160+ "title" : "test" ,
161+ "writeOnly" : False ,
162+ "readOnly" : True ,
163+ "type" : "integer" ,
164+ "forms" : [],
165+ },
166+ "writeonly" : {
167+ "title" : "test" ,
168+ "writeOnly" : True ,
169+ "readOnly" : False ,
170+ "type" : "integer" ,
171+ "forms" : [],
172+ },
173+ },
174+ "actions" : {},
175+ "base" : None ,
176+ "securityDefinitions" : {"no_security" : {}},
177+ "security" : "no_security" ,
178+ }
179+
180+ # Create a client object
181+ MyClient = lt .ThingClient .subclass_from_td (thing_description )
182+ client = MyClient ("/" , mocker .Mock ())
183+ # Mock the underlying get/set functions so we don't need a server
184+ mocker .patch .object (client , "get_property" )
185+ client .get_property .return_value = 42
186+ mocker .patch .object (client , "set_property" )
187+
188+ # Check which properties we can read
189+ assert client .readwrite == 42
190+ assert client .readonly == 42
191+ with pytest .raises (ClientPropertyError , match = "may not be read" ):
192+ _ = client .writeonly
193+ assert client .get_property .call_count == 2
194+
195+ # The same check for writing
196+ client .readwrite = 0
197+ client .writeonly = 0
198+ with pytest .raises (ClientPropertyError , match = "may not be set" ):
199+ _ = client .readonly = 0
200+ assert client .set_property .call_count == 2
201+
202+
203+ def test_call_action (thing_client_and_thing ):
116204 """Test calling an action."""
205+ thing_client , thing = thing_client_and_thing
117206 assert thing_client .int_prop == 1
118207 thing_client .increment ()
119208 assert thing_client .int_prop == 2
209+ assert thing .int_prop == 2
120210
121211
122- def test_call_action_with_return (thing_client ):
212+ def test_call_action_with_return (thing_client_and_thing ):
123213 """Test calling an action with a return."""
214+ thing_client , thing = thing_client_and_thing
124215 assert thing_client .int_prop == 1
125216 new_value = thing_client .increment_and_return ()
126217 assert new_value == 2
127218 assert thing_client .int_prop == 2
219+ assert thing .int_prop == 2
128220
129221
130- def test_call_action_with_args (thing_client ):
222+ def test_call_action_with_args (thing_client_and_thing ):
131223 """Test calling an action."""
224+ thing_client , thing = thing_client_and_thing
132225 assert thing_client .int_prop == 1
133226 thing_client .increment_by_input (value = 5 )
134227 assert thing_client .int_prop == 6
228+ assert thing .int_prop == 6
135229
136230
137- def test_call_action_with_args_and_return (thing_client ):
231+ def test_call_action_with_args_and_return (thing_client_and_thing ):
138232 """Test calling an action with a return."""
233+ thing_client , thing = thing_client_and_thing
139234 assert thing_client .int_prop == 1
140235 new_value = thing_client .increment_by_input_and_return (value = 5 )
141236 assert new_value == 6
142237 assert thing_client .int_prop == 6
238+ assert thing .int_prop == 6
143239
144240
145241def test_call_action_wrong_arg (thing_client ):
146242 """Test calling an action with wrong argument."""
147243 err = "Error when invoking action increment_by_input: 'value' - Field required"
148244
149- with pytest .raises (lt . exceptions . FailedToInvokeActionError , match = err ):
245+ with pytest .raises (FailedToInvokeActionError , match = err ):
150246 thing_client .increment_by_input (input = 5 )
151247
152248
@@ -156,7 +252,7 @@ def test_call_action_wrong_type(thing_client):
156252 "Error when invoking action increment_by_input: 'value' - Input should be a "
157253 "valid integer, unable to parse string as an integer"
158254 )
159- with pytest .raises (lt . exceptions . FailedToInvokeActionError , match = err ):
255+ with pytest .raises (FailedToInvokeActionError , match = err ):
160256 thing_client .increment_by_input (value = "foo" )
161257
162258
0 commit comments