+"""
+tests for controller
+"""
# ==================================================================================
-# Copyright (c) 2019 Nokia
-# Copyright (c) 2018-2019 AT&T Intellectual Property.
+# 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.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==================================================================================
-import tempfile
-import os
+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
-from rmr.rmr_mocks import rmr_mocks
-from a1 import app
-import testing_helpers
-import pytest
+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)]
-ADM_CTRL = "admission_control_policy"
-ADM_CTRL_INSTANCE = "/a1-p/policytypes/20000/policies/" + ADM_CTRL
-ADM_CTRL_INSTANCE_STATUS = ADM_CTRL_INSTANCE + "/status"
+def _fake_dequeue_none():
+ """for monkeypatching with no waiting messages"""
+ return []
-# http://flask.pocoo.org/docs/1.0/testing/
-@pytest.fixture
-def client():
- db_fd, app.app.config["DATABASE"] = tempfile.mkstemp()
- app.app.config["TESTING"] = True
- cl = app.app.test_client()
- yield cl
+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()
- os.close(db_fd)
- os.unlink(app.app.config["DATABASE"])
+ # 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))
-def _fake_dequeue(_filter_type):
- """
- for monkeypatching a1rmnr.dequeue_all_messages
- """
- fake_msg = {}
- pay = b'{"policy_type_id": 20000, "policy_instance_id": "admission_control_policy", "handler_id": "test_receiver", "status": "OK"}'
- fake_msg["payload"] = pay
- new_messages = [fake_msg]
- return new_messages
+ # 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):
- testing_helpers.patch_all(monkeypatch)
- monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(0)) # good sends for this whole batch
+ 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))
- # we need to repatch alloc (already patched in patch_rmr) to fix the transactionid, alloc is called in send and recieve
- def fake_alloc(_unused, _alsounused):
- sbuf = rmr_mocks.Rmr_mbuf_t()
- sbuf.contents.xaction = b"d49b53e478b711e9a1130242ac110002"
- return sbuf
- # we also need to repatch set, since in the send function, we alloc, then set a new transid
- def fake_set_transactionid(sbuf):
- sbuf.contents.xaction = b"d49b53e478b711e9a1130242ac110002"
+def _no_ac(client):
+ # no type there yet
+ res = client.get(ADM_CTRL_TYPE)
+ assert res.status_code == 404
- # Note, we could have just patched summary, but this patches at a "lower level" so is a better test
- monkeypatch.setattr("rmr.rmr.rmr_alloc_msg", fake_alloc)
- monkeypatch.setattr("rmr.rmr.generate_and_set_transaction_id", fake_set_transactionid)
+ # 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
-# Actual Tests
+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 test_policy_get(client, monkeypatch):
-# """
-# test policy GET
-# """
-# _test_put_patch(monkeypatch)
-# monkeypatch.setattr(
-# "a1.a1rmr.dequeue_all_waiting_messages",
-# _fake_dequeue(monkeypatch, msg_payload={"GET ack": "pretend policy is here"}, msg_type=20003),
-# )
-# res = client.get("/a1-p/policies/admission_control_policy")
-# assert res.status_code == 200
-# assert res.json == {"GET ack": "pretend policy is here"}
-#
-#
-# def test_policy_get_unsupported(client, monkeypatch):
-# """
-# test policy GET
-# """
-# testing_helpers.patch_all(monkeypatch, nofetch=True)
-# res = client.get("/a1-p/policies/admission_control_policy")
-# assert res.status_code == 400
-# assert res.data == b'"POLICY DOES NOT SUPPORT FETCHING"\n'
-#
-#
-def test_xapp_put_good(client, monkeypatch):
- """ test policy put good"""
- # nothing there yet
+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)
# create a good instance
_test_put_patch(monkeypatch)
- res = client.put(ADM_CTRL_INSTANCE, json=testing_helpers.good_payload())
- assert res.status_code == 201
+ res = client.put(ADM_CTRL_INSTANCE, json=instancedef)
+ assert res.status_code == 202
- # get the instance
- res = client.get(ADM_CTRL_INSTANCE)
+ # 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 == testing_helpers.good_payload()
+ 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)
- # get the instance status
- monkeypatch.setattr("a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue)
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 == [{"handler_id": "test_receiver", "status": "OK"}]
+ assert res.json == []
- # assert that rmr bad states don't cause problems
- monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
- res = client.put(ADM_CTRL_INSTANCE, json=testing_helpers.good_payload())
- assert res.status_code == 201
+ # cant get instance
+ res = client.get(ADM_CTRL_INSTANCE)
+ assert res.status_code == 404
- monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(5))
- res = client.put(ADM_CTRL_INSTANCE, json=testing_helpers.good_payload())
- assert res.status_code == 201
+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
-#
-#
-# def test_xapp_put_bad(client, monkeypatch):
-# """Test policy put fails"""
-# _test_put_patch(monkeypatch)
-# # return from policy handler has a status indicating FAIL
-# monkeypatch.setattr(
-# "a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_payload={"status": "FAIL", "foo": "bar"})
-# )
-# res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-# assert res.status_code == 502
-# assert res.json["reason"] == "BAD STATUS"
-# assert res.json["return_payload"] == {"status": "FAIL", "foo": "bar"}
-#
-# # return from policy handler has no status field
-# monkeypatch.setattr("a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_payload={"foo": "bar"}))
-# res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-# assert res.status_code == 502
-# assert res.json["reason"] == "NO STATUS"
-# assert res.json["return_payload"] == {"foo": "bar"}
-#
-# # return from policy handler not a json
-# monkeypatch.setattr(
-# "a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_payload="booger", jsonb=False)
-# )
-# res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-# assert res.status_code == 502
-# assert res.json["reason"] == "NOT JSON"
-# assert res.json["return_payload"] == "booger"
-#
-# # bad type
-# monkeypatch.setattr("a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_type=666))
-# res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-# assert res.status_code == 504
-# assert res.data == b"\"A1 was expecting an ACK back but it didn't receive one or didn't recieve the expected ACK\"\n"
-#
-# # bad state
-# monkeypatch.setattr("a1.a1rmr.dequeue_all_waiting_messages", _fake_dequeue(monkeypatch, msg_state=666))
-# res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-# assert res.status_code == 504
-# assert res.data == b"\"A1 was expecting an ACK back but it didn't receive one or didn't recieve the expected ACK\"\n"
-#
-#
-def test_bad_requests(client, monkeypatch):
+ 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 bad send failures
+ test various failure modes
"""
- testing_helpers.patch_all(monkeypatch)
+ # 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
res = client.put(ADM_CTRL_INSTANCE, data="notajson")
assert res.status_code == 415
- # test a PUT body against a poliucy not expecting one
- res = client.put("/a1-p/policytypes/20001/policies/test_policy", json=testing_helpers.good_payload())
- assert res.status_code == 400
- assert res.data == b'"BODY SUPPLIED BUT POLICY HAS NO EXPECTED BODY"\n'
+ # 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
-# def test_bad_requests(client, monkeypatch):
-# """Test bad requests"""
-# testing_helpers.patch_all(monkeypatch)
-#
-# # test a 404
-# res = client.put("/a1-p/policies/noexist", json=testing_helpers.good_payload())
-# 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 test_missing_manifest(client, monkeypatch):
-# """
-# test that we get a 500 with an approrpiate message on a missing manifest
-# """
-#
-# def f():
-# raise exceptions.MissingManifest()
-#
-# monkeypatch.setattr("a1.utils.get_ric_manifest", f)
-#
-# res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-# assert res.status_code == 500
-# assert res.data == b'"A1 was unable to find the required RIC manifest. report this!"\n'
-#
-#
+ 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):
"""
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()