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