-"""
-tests for controller
-"""
-# ==================================================================================
-# Copyright (c) 2019-2020 Nokia
-# Copyright (c) 2018-2020 AT&T Intellectual Property.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-# ==================================================================================
-import time
-import json
-from ricxappframe.rmr.rmr_mocks import rmr_mocks
-from ricxappframe.xapp_sdl import SDLWrapper
-from ricsdl.exceptions import RejectedByBackend, NotConnected, BackendError
-from a1 import a1rmr, data
-
-RCV_ID = "test_receiver"
-ADM_CRTL_TID = 6660666
-ADM_CTRL_IID = "admission_control_policy"
-ADM_CTRL_POLICIES = "/a1-p/policytypes/{0}/policies".format(ADM_CRTL_TID)
-ADM_CTRL_INSTANCE = ADM_CTRL_POLICIES + "/" + ADM_CTRL_IID
-ADM_CTRL_INSTANCE_STATUS = ADM_CTRL_INSTANCE + "/status"
-ADM_CTRL_TYPE = "/a1-p/policytypes/{0}".format(ADM_CRTL_TID)
-ACK_MT = 20011
-
-
-def _fake_dequeue():
- """for monkeypatching with a good status"""
- pay = json.dumps(
- {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "OK"}
- ).encode()
- fake_msg = {"payload": pay, "message type": ACK_MT}
- return [(fake_msg, None)]
-
-
-def _fake_dequeue_none():
- """for monkeypatching with no waiting messages"""
- return []
-
-
-def _fake_dequeue_deleted():
- """for monkeypatching with a DELETED status"""
- new_msgs = []
- good_pay = json.dumps(
- {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "DELETED"}
- ).encode()
-
- # non existent type id
- pay = json.dumps(
- {"policy_type_id": 911, "policy_instance_id": ADM_CTRL_IID, "handler_id": RCV_ID, "status": "DELETED"}
- ).encode()
- fake_msg = {"payload": pay, "message type": ACK_MT}
- new_msgs.append((fake_msg, None))
-
- # bad instance id
- pay = json.dumps(
- {"policy_type_id": ADM_CRTL_TID, "policy_instance_id": "darkness", "handler_id": RCV_ID, "status": "DELETED"}
- ).encode()
- fake_msg = {"payload": pay, "message type": ACK_MT}
- new_msgs.append((fake_msg, None))
-
- # good body but bad message type
- fake_msg = {"payload": good_pay, "message type": ACK_MT * 3}
- new_msgs.append((fake_msg, None))
-
- # insert a bad one with a malformed body to make sure we keep going
- new_msgs.append(({"payload": "asdf", "message type": ACK_MT}, None))
-
- # not even a json
- new_msgs.append(("asdf", None))
-
- # good
- fake_msg = {"payload": good_pay, "message type": ACK_MT}
- new_msgs.append((fake_msg, None))
-
- return new_msgs
-
-
-def _test_put_patch(monkeypatch):
- rmr_mocks.patch_rmr(monkeypatch)
- # assert that rmr bad states don't cause problems
- monkeypatch.setattr("ricxappframe.rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
-
-
-def _no_ac(client):
- # no type there yet
- res = client.get(ADM_CTRL_TYPE)
- assert res.status_code == 404
-
- # no types at all
- res = client.get("/a1-p/policytypes")
- assert res.status_code == 200
- assert res.json == []
-
- # instance 404 because type not there yet
- res = client.get(ADM_CTRL_POLICIES)
- assert res.status_code == 404
-
-
-def _put_ac_type(client, typedef):
- _no_ac(client)
-
- # put the type
- res = client.put(ADM_CTRL_TYPE, json=typedef)
- assert res.status_code == 201
-
- # cant replace types
- res = client.put(ADM_CTRL_TYPE, json=typedef)
- assert res.status_code == 400
-
- # type there now
- res = client.get(ADM_CTRL_TYPE)
- assert res.status_code == 200
- assert res.json == typedef
-
- # type in type list
- res = client.get("/a1-p/policytypes")
- assert res.status_code == 200
- assert res.json == [ADM_CRTL_TID]
-
- # instance 200 but empty list
- res = client.get(ADM_CTRL_POLICIES)
- assert res.status_code == 200
- assert res.json == []
-
-
-def _delete_ac_type(client):
- res = client.delete(ADM_CTRL_TYPE)
- assert res.status_code == 204
-
- # cant get
- res = client.get(ADM_CTRL_TYPE)
- assert res.status_code == 404
-
- # cant invoke delete on it again
- res = client.delete(ADM_CTRL_TYPE)
- assert res.status_code == 404
-
- _no_ac(client)
-
-
-def _put_ac_instance(client, monkeypatch, instancedef):
- # no instance there yet
- res = client.get(ADM_CTRL_INSTANCE)
- assert res.status_code == 404
- res = client.get(ADM_CTRL_INSTANCE_STATUS)
- assert res.status_code == 404
-
- # create a good instance
- _test_put_patch(monkeypatch)
- res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
- assert res.status_code == 202
-
- # replace is allowed on instances
- res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
- assert res.status_code == 202
-
- # instance 200 and in list
- res = client.get(ADM_CTRL_POLICIES)
- assert res.status_code == 200
- assert res.json == [ADM_CTRL_IID]
-
-
-def _delete_instance(client):
- # cant delete type until there are no instances
- res = client.delete(ADM_CTRL_TYPE)
- assert res.status_code == 400
-
- # delete it
- res = client.delete(ADM_CTRL_INSTANCE)
- assert res.status_code == 202
-
- # should be able to do multiple deletes until it's actually gone
- res = client.delete(ADM_CTRL_INSTANCE)
- assert res.status_code == 202
-
-
-def _instance_is_gone(client, seconds_to_try=10):
- for _ in range(seconds_to_try):
- # idea here is that we have to wait for the seperate thread to process the event
- try:
- res = client.get(ADM_CTRL_INSTANCE_STATUS)
- assert res.status_code == 404
- except AssertionError:
- time.sleep(1)
-
- res = client.get(ADM_CTRL_INSTANCE_STATUS)
- assert res.status_code == 404
-
- # list still 200 but no instance
- res = client.get(ADM_CTRL_POLICIES)
- assert res.status_code == 200
- assert res.json == []
-
- # cant get instance
- res = client.get(ADM_CTRL_INSTANCE)
- assert res.status_code == 404
-
-
-def _verify_instance_and_status(client, expected_instance, expected_status, expected_deleted, seconds_to_try=5):
- # get the instance
- res = client.get(ADM_CTRL_INSTANCE)
- assert res.status_code == 200
- assert res.json == expected_instance
-
- for _ in range(seconds_to_try):
- # idea here is that we have to wait for the seperate thread to process the event
- res = client.get(ADM_CTRL_INSTANCE_STATUS)
- assert res.status_code == 200
- assert res.json["has_been_deleted"] == expected_deleted
- try:
- assert res.json["instance_status"] == expected_status
- return
- except AssertionError:
- time.sleep(1)
- assert res.json["instance_status"] == expected_status
-
-
-# Module level Hack
-
-
-def setup_module():
- """module level setup"""
-
- # swap sdl for the fake backend
- data.SDL = SDLWrapper(use_fake_sdl=True)
-
- def noop():
- pass
-
- # launch the thread with a fake init func and a patched rcv func; we will "repatch" later
- a1rmr.start_rmr_thread(init_func_override=noop, rcv_func_override=_fake_dequeue_none)
-
-
-# Actual Tests
-
-
-def test_workflow(client, monkeypatch, adm_type_good, adm_instance_good):
- """
- test a full A1 workflow
- """
-
- # put type and instance
- _put_ac_type(client, adm_type_good)
- _put_ac_instance(client, monkeypatch, adm_instance_good)
-
- """
- we test the state transition diagram of all 5 states here;
- 1. not in effect, not deleted
- 2. in effect, not deleted
- 3. in effect, deleted
- 4. not in effect, deleted
- 5. gone (timeout expires)
- """
-
- # try a status get but we didn't get any ACKs yet to test NOT IN EFFECT
- _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", False)
-
- # now pretend we did get a good ACK
- a1rmr.replace_rcv_func(_fake_dequeue)
- _verify_instance_and_status(client, adm_instance_good, "IN EFFECT", False)
-
- # delete the instance
- _delete_instance(client)
-
- # status after a delete, but there are no messages yet, should still return
- _verify_instance_and_status(client, adm_instance_good, "IN EFFECT", True)
-
- # now pretend we deleted successfully
- a1rmr.replace_rcv_func(_fake_dequeue_deleted)
-
- # status should be reflected first (before delete triggers)
- _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
-
- # instance should be totally gone after a few seconds
- _instance_is_gone(client)
-
- # delete the type
- _delete_ac_type(client)
-
-
-def test_cleanup_via_t1(client, monkeypatch, adm_type_good, adm_instance_good):
- """
- create a type, create an instance, but no acks ever come in, delete instance
- """
- _put_ac_type(client, adm_type_good)
-
- a1rmr.replace_rcv_func(_fake_dequeue_none)
-
- _put_ac_instance(client, monkeypatch, adm_instance_good)
-
- """
- here we test the state transition diagram when it never goes into effect:
- 1. not in effect, not deleted
- 2. not in effect, deleted
- 3. gone (timeout expires)
- """
-
- _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", False)
-
- # delete the instance
- _delete_instance(client)
-
- _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
-
- # instance should be totally gone after a few seconds
- _instance_is_gone(client)
-
- # delete the type
- _delete_ac_type(client)
-
-
-def test_bad_instances(client, monkeypatch, adm_type_good):
- """
- test various failure modes
- """
- # put the type (needed for some of the tests below)
- rmr_mocks.patch_rmr(monkeypatch)
- res = client.put(ADM_CTRL_TYPE, json=adm_type_good)
- assert res.status_code == 201
-
- # bad body
- res = client.put(ADM_CTRL_INSTANCE, json={"not": "expected"})
- assert res.status_code == 400
-
- # bad media type
- res = client.put(ADM_CTRL_INSTANCE, data="notajson")
- assert res.status_code == 415
-
- # delete a non existent instance
- res = client.delete(ADM_CTRL_INSTANCE + "DARKNESS")
- assert res.status_code == 404
-
- # get a non existent instance
- a1rmr.replace_rcv_func(_fake_dequeue)
- res = client.get(ADM_CTRL_INSTANCE + "DARKNESS")
- assert res.status_code == 404
-
- # delete the type (as cleanup)
- res = client.delete(ADM_CTRL_TYPE)
- assert res.status_code == 204
-
- # test 503 handlers
-
- def monkey_set(ns, key, value):
- # set a key override function that throws sdl errors on certain keys
- if key == "a1.policy_type.111":
- raise RejectedByBackend()
- if key == "a1.policy_type.112":
- raise NotConnected()
- if key == "a1.policy_type.113":
- raise BackendError()
-
- monkeypatch.setattr("a1.data.SDL.set", monkey_set)
-
- def create_alt_id(json, id):
- """
- Overwrites the json's policy type ID, attempts create and tests for 503
- """
- json['policy_type_id'] = id
- url = "/a1-p/policytypes/{0}".format(id)
- res = client.put(url, json=json)
- assert res.status_code == 503
-
- create_alt_id(adm_type_good, 111)
- create_alt_id(adm_type_good, 112)
- create_alt_id(adm_type_good, 113)
-
-
-def test_illegal_types(client, adm_type_good):
- """
- Test illegal types
- """
- # below valid range
- res = client.put("/a1-p/policytypes/0", json=adm_type_good)
- assert res.status_code == 400
- # ID mismatch
- res = client.put("/a1-p/policytypes/1", json=adm_type_good)
- assert res.status_code == 400
- # above valid range
- res = client.put("/a1-p/policytypes/2147483648", json=adm_type_good)
- assert res.status_code == 400
-
-
-def test_healthcheck(client):
- """
- test healthcheck
- """
- res = client.get("/a1-p/healthcheck")
- assert res.status_code == 200
-
-
-def test_metrics(client):
- """
- test Prometheus metrics
- """
- res = client.get("/a1-p/metrics")
- assert res.status_code == 200
-
-
-def teardown_module():
- """module teardown"""
- a1rmr.stop_rmr_thread()