Revise custom exceptions to require a message
[ric-plt/a1.git] / tests / test_controller.py
1 """
2 tests for controller
3 """
4 # ==================================================================================
5 #       Copyright (c) 2019-2020 Nokia
6 #       Copyright (c) 2018-2020 AT&T Intellectual Property.
7 #
8 #   Licensed under the Apache License, Version 2.0 (the "License");
9 #   you may not use this file except in compliance with the License.
10 #   You may obtain a copy of the License at
11 #
12 #          http://www.apache.org/licenses/LICENSE-2.0
13 #
14 #   Unless required by applicable law or agreed to in writing, software
15 #   distributed under the License is distributed on an "AS IS" BASIS,
16 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 #   See the License for the specific language governing permissions and
18 #   limitations under the License.
19 # ==================================================================================
20 import time
21 import json
22 from ricxappframe.rmr.rmr_mocks import rmr_mocks
23 from ricxappframe.xapp_sdl import SDLWrapper
24 from ricsdl.exceptions import RejectedByBackend, NotConnected, BackendError
25 from a1 import a1rmr, data
26
27 RCV_ID = "test_receiver"
28 ADM_CRTL_TID = 6660666
29 ADM_CTRL_IID = "admission_control_policy"
30 ADM_CTRL_POLICIES = "/a1-p/policytypes/{0}/policies".format(ADM_CRTL_TID)
31 ADM_CTRL_INSTANCE = ADM_CTRL_POLICIES + "/" + ADM_CTRL_IID
32 ADM_CTRL_INSTANCE_STATUS = ADM_CTRL_INSTANCE + "/status"
33 ADM_CTRL_TYPE = "/a1-p/policytypes/{0}".format(ADM_CRTL_TID)
34 ACK_MT = 20011
35
36
37 def _fake_dequeue():
38     """for monkeypatching with a good status"""
39     pay = json.dumps(
40         {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "OK"}
41     ).encode()
42     fake_msg = {"payload": pay, "message type": ACK_MT}
43     return [(fake_msg, None)]
44
45
46 def _fake_dequeue_none():
47     """for monkeypatching with no waiting messages"""
48     return []
49
50
51 def _fake_dequeue_deleted():
52     """for monkeypatching  with a DELETED status"""
53     new_msgs = []
54     good_pay = json.dumps(
55         {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "DELETED"}
56     ).encode()
57
58     # non existent type id
59     pay = json.dumps(
60         {"policy_type_id": 911, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "DELETED"}
61     ).encode()
62     fake_msg = {"payload": pay, "message type": ACK_MT}
63     new_msgs.append((fake_msg, None))
64
65     # bad instance id
66     pay = json.dumps(
67         {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": "darkness", "handler_id": RCV_ID, "status": "DELETED"}
68     ).encode()
69     fake_msg = {"payload": pay, "message type": ACK_MT}
70     new_msgs.append((fake_msg, None))
71
72     # good body but bad message type
73     fake_msg = {"payload": good_pay, "message type": ACK_MT * 3}
74     new_msgs.append((fake_msg, None))
75
76     # insert a bad one with a malformed body to make sure we keep going
77     new_msgs.append(({"payload": "asdf", "message type": ACK_MT}, None))
78
79     # not even a json
80     new_msgs.append(("asdf", None))
81
82     # good
83     fake_msg = {"payload": good_pay, "message type": ACK_MT}
84     new_msgs.append((fake_msg, None))
85
86     return new_msgs
87
88
89 def _test_put_patch(monkeypatch):
90     rmr_mocks.patch_rmr(monkeypatch)
91     # assert that rmr bad states don't cause problems
92     monkeypatch.setattr("ricxappframe.rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
93
94
95 def _no_ac(client):
96     # no type there yet
97     res = client.get(ADM_CTRL_TYPE)
98     assert res.status_code == 404
99
100     # no types at all
101     res = client.get("/a1-p/policytypes")
102     assert res.status_code == 200
103     assert res.json == []
104
105     # instance 404 because type not there yet
106     res = client.get(ADM_CTRL_POLICIES)
107     assert res.status_code == 404
108
109
110 def _put_ac_type(client, typedef):
111     _no_ac(client)
112
113     # put the type
114     res = client.put(ADM_CTRL_TYPE, json=typedef)
115     assert res.status_code == 201
116
117     # cant replace types
118     res = client.put(ADM_CTRL_TYPE, json=typedef)
119     assert res.status_code == 400
120
121     # type there now
122     res = client.get(ADM_CTRL_TYPE)
123     assert res.status_code == 200
124     assert res.json == typedef
125
126     # type in type list
127     res = client.get("/a1-p/policytypes")
128     assert res.status_code == 200
129     assert res.json == [ADM_CRTL_TID]
130
131     # instance 200 but empty list
132     res = client.get(ADM_CTRL_POLICIES)
133     assert res.status_code == 200
134     assert res.json == []
135
136
137 def _delete_ac_type(client):
138     res = client.delete(ADM_CTRL_TYPE)
139     assert res.status_code == 204
140
141     # cant get
142     res = client.get(ADM_CTRL_TYPE)
143     assert res.status_code == 404
144
145     # cant invoke delete on it again
146     res = client.delete(ADM_CTRL_TYPE)
147     assert res.status_code == 404
148
149     _no_ac(client)
150
151
152 def _put_ac_instance(client, monkeypatch, instancedef):
153     # no instance there yet
154     res = client.get(ADM_CTRL_INSTANCE)
155     assert res.status_code == 404
156     res = client.get(ADM_CTRL_INSTANCE_STATUS)
157     assert res.status_code == 404
158
159     # create a good instance
160     _test_put_patch(monkeypatch)
161     res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
162     assert res.status_code == 202
163
164     # replace is allowed on instances
165     res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
166     assert res.status_code == 202
167
168     # instance 200 and in list
169     res = client.get(ADM_CTRL_POLICIES)
170     assert res.status_code == 200
171     assert res.json == [ADM_CTRL_IID]
172
173
174 def _delete_instance(client):
175     # cant delete type until there are no instances
176     res = client.delete(ADM_CTRL_TYPE)
177     assert res.status_code == 400
178
179     # delete it
180     res = client.delete(ADM_CTRL_INSTANCE)
181     assert res.status_code == 202
182
183     # should be able to do multiple deletes until it's actually gone
184     res = client.delete(ADM_CTRL_INSTANCE)
185     assert res.status_code == 202
186
187
188 def _instance_is_gone(client, seconds_to_try=10):
189     for _ in range(seconds_to_try):
190         # idea here is that we have to wait for the seperate thread to process the event
191         try:
192             res = client.get(ADM_CTRL_INSTANCE_STATUS)
193             assert res.status_code == 404
194         except AssertionError:
195             time.sleep(1)
196
197     res = client.get(ADM_CTRL_INSTANCE_STATUS)
198     assert res.status_code == 404
199
200     # list still 200 but no instance
201     res = client.get(ADM_CTRL_POLICIES)
202     assert res.status_code == 200
203     assert res.json == []
204
205     # cant get instance
206     res = client.get(ADM_CTRL_INSTANCE)
207     assert res.status_code == 404
208
209
210 def _verify_instance_and_status(client, expected_instance, expected_status, expected_deleted, seconds_to_try=5):
211     # get the instance
212     res = client.get(ADM_CTRL_INSTANCE)
213     assert res.status_code == 200
214     assert res.json == expected_instance
215
216     for _ in range(seconds_to_try):
217         # idea here is that we have to wait for the seperate thread to process the event
218         res = client.get(ADM_CTRL_INSTANCE_STATUS)
219         assert res.status_code == 200
220         assert res.json["has_been_deleted"] == expected_deleted
221         try:
222             assert res.json["instance_status"] == expected_status
223             return
224         except AssertionError:
225             time.sleep(1)
226     assert res.json["instance_status"] == expected_status
227
228
229 # Module level Hack
230
231
232 def setup_module():
233     """module level setup"""
234
235     # swap sdl for the fake backend
236     data.SDL = SDLWrapper(use_fake_sdl=True)
237
238     def noop():
239         pass
240
241     # launch the thread with a fake init func and a patched rcv func; we will "repatch" later
242     a1rmr.start_rmr_thread(init_func_override=noop, rcv_func_override=_fake_dequeue_none)
243
244
245 # Actual Tests
246
247
248 def test_workflow(client, monkeypatch, adm_type_good, adm_instance_good):
249     """
250     test a full A1 workflow
251     """
252
253     # put type and instance
254     _put_ac_type(client, adm_type_good)
255     _put_ac_instance(client, monkeypatch, adm_instance_good)
256
257     """
258     we test the state transition diagram of all 5 states here;
259     1. not in effect, not deleted
260     2. in effect, not deleted
261     3. in effect, deleted
262     4. not in effect, deleted
263     5. gone (timeout expires)
264     """
265
266     # try a status get but we didn't get any ACKs yet to test NOT IN EFFECT
267     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", False)
268
269     # now pretend we did get a good ACK
270     a1rmr.replace_rcv_func(_fake_dequeue)
271     _verify_instance_and_status(client, adm_instance_good, "IN EFFECT", False)
272
273     # delete the instance
274     _delete_instance(client)
275
276     # status after a delete, but there are no messages yet, should still return
277     _verify_instance_and_status(client, adm_instance_good, "IN EFFECT", True)
278
279     # now pretend we deleted successfully
280     a1rmr.replace_rcv_func(_fake_dequeue_deleted)
281
282     # status should be reflected first (before delete triggers)
283     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
284
285     # instance should be totally gone after a few seconds
286     _instance_is_gone(client)
287
288     # delete the type
289     _delete_ac_type(client)
290
291
292 def test_cleanup_via_t1(client, monkeypatch, adm_type_good, adm_instance_good):
293     """
294     create a type, create an instance, but no acks ever come in, delete instance
295     """
296     _put_ac_type(client, adm_type_good)
297
298     a1rmr.replace_rcv_func(_fake_dequeue_none)
299
300     _put_ac_instance(client, monkeypatch, adm_instance_good)
301
302     """
303     here we test the state transition diagram when it never goes into effect:
304     1. not in effect, not deleted
305     2. not in effect, deleted
306     3. gone (timeout expires)
307     """
308
309     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", False)
310
311     # delete the instance
312     _delete_instance(client)
313
314     _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
315
316     # instance should be totally gone after a few seconds
317     _instance_is_gone(client)
318
319     # delete the type
320     _delete_ac_type(client)
321
322
323 def test_bad_instances(client, monkeypatch, adm_type_good):
324     """
325     test various failure modes
326     """
327     # put the type (needed for some of the tests below)
328     rmr_mocks.patch_rmr(monkeypatch)
329     res = client.put(ADM_CTRL_TYPE, json=adm_type_good)
330     assert res.status_code == 201
331
332     # bad body
333     res = client.put(ADM_CTRL_INSTANCE, json={"not": "expected"})
334     assert res.status_code == 400
335
336     # bad media type
337     res = client.put(ADM_CTRL_INSTANCE, data="notajson")
338     assert res.status_code == 415
339
340     # delete a non existent instance
341     res = client.delete(ADM_CTRL_INSTANCE + "DARKNESS")
342     assert res.status_code == 404
343
344     # get a non existent instance
345     a1rmr.replace_rcv_func(_fake_dequeue)
346     res = client.get(ADM_CTRL_INSTANCE + "DARKNESS")
347     assert res.status_code == 404
348
349     # delete the type (as cleanup)
350     res = client.delete(ADM_CTRL_TYPE)
351     assert res.status_code == 204
352
353     # test 503 handlers
354
355     def monkey_set(ns, key, value):
356         # set a key override function that throws sdl errors on certain keys
357         if key == "a1.policy_type.111":
358             raise RejectedByBackend()
359         if key == "a1.policy_type.112":
360             raise NotConnected()
361         if key == "a1.policy_type.113":
362             raise BackendError()
363
364     monkeypatch.setattr("a1.data.SDL.set", monkey_set)
365
366     def create_alt_id(json, id):
367         """
368         Overwrites the json's policy type ID, attempts create and tests for 503
369         """
370         json['policy_type_id'] = id
371         url = "/a1-p/policytypes/{0}".format(id)
372         res = client.put(url, json=json)
373         assert res.status_code == 503
374
375     create_alt_id(adm_type_good, 111)
376     create_alt_id(adm_type_good, 112)
377     create_alt_id(adm_type_good, 113)
378
379
380 def test_illegal_types(client, adm_type_good):
381     """
382     Test illegal types
383     """
384     # below valid range
385     res = client.put("/a1-p/policytypes/0", json=adm_type_good)
386     assert res.status_code == 400
387     # ID mismatch
388     res = client.put("/a1-p/policytypes/1", json=adm_type_good)
389     assert res.status_code == 400
390     # above valid range
391     res = client.put("/a1-p/policytypes/2147483648", json=adm_type_good)
392     assert res.status_code == 400
393
394
395 def test_healthcheck(client):
396     """
397     test healthcheck
398     """
399     res = client.get("/a1-p/healthcheck")
400     assert res.status_code == 200
401
402
403 def teardown_module():
404     """module teardown"""
405     a1rmr.stop_rmr_thread()