Move to ricxappframe rmr, rmr3.6.3
[ric-plt/a1.git] / tests / test_controller.py
index bae2ac9..2aac5a9 100644 (file)
@@ -1,6 +1,9 @@
+"""
+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
-from rmr.rmr_mocks import rmr_mocks
-from a1 import app
-from a1 import exceptions
-from rmr import rmr
-import testing_helpers
-import pytest
-
-# 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
-
-    os.close(db_fd)
-    os.unlink(app.app.config["DATABASE"])
-
-
-def _fake_dequeue(
-    monkeypatch,
-    msg_payload={"status": "SUCCESS", "foo": "bar"},
-    msg_type=20001,
-    msg_state=0,
-    jsonb=True,
-    unexpected_first=True,
-):
-    """
-    generates a mock rmr message response (returns a function that does; uses closures to set params)
-    """
-    new_messages = []
-    # stick a message we don't want at the front of the queue, then stick the message we want
-    if unexpected_first:
-        monkeypatch.setattr("rmr.rmr.rmr_torcv_msg", rmr_mocks.rcv_mock_generator(msg_payload, -1, msg_state, jsonb))
-        sbuf = rmr.rmr_alloc_msg(None, None)
-        sbuf = rmr.rmr_torcv_msg(None, sbuf, None)
-        summary = rmr.message_summary(sbuf)
-        new_messages.append(summary)
+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
 
-    monkeypatch.setattr("rmr.rmr.rmr_torcv_msg", rmr_mocks.rcv_mock_generator(msg_payload, msg_type, msg_state, jsonb))
-    sbuf = rmr.rmr_alloc_msg(None, None)
-    sbuf = rmr.rmr_torcv_msg(None, sbuf, None)
-    summary = rmr.message_summary(sbuf)
-    new_messages.append(summary)
 
-    def f():
-        return new_messages
+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))
 
-    return f
 
+def _no_ac(client):
+    # no type there yet
+    res = client.get(ADM_CTRL_TYPE)
+    assert res.status_code == 404
 
-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
+    # 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
 
-    # 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
+    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
 
-    # 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"
 
-    # 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)
+# 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_policy_get(client, monkeypatch):
+def test_workflow(client, monkeypatch, adm_type_good, adm_instance_good):
     """
-    test policy GET
+    test a full A1 workflow
     """
-    _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"}
 
+    # put type and instance
+    _put_ac_type(client, adm_type_good)
+    _put_ac_instance(client, monkeypatch, adm_instance_good)
 
-def test_policy_get_unsupported(client, monkeypatch):
     """
-    test policy GET
+    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)
     """
-    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'
 
+    # 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)
 
-def test_xapp_put_good(client, monkeypatch):
-    """ test policy put good"""
-    _test_put_patch(monkeypatch)
-    monkeypatch.setattr("a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch))
-    res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-    assert res.status_code == 200
-    assert res.json == {"status": "SUCCESS", "foo": "bar"}
+    # 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)
 
-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_xapp_put_bad_send(client, monkeypatch):
+    # 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):
     """
-    Test bad send failures
+    create a type, create an instance, but no acks ever come in, delete instance
     """
-    testing_helpers.patch_all(monkeypatch)
+    _put_ac_type(client, adm_type_good)
 
-    monkeypatch.setattr("a1.a1rmr._dequeue_all_waiting_messages", _fake_dequeue(monkeypatch))
-    res = client.put("/a1-p/policies/admission_control_policy", json={"not": "expected"})
-    assert res.status_code == 400
+    a1rmr.replace_rcv_func(_fake_dequeue_none)
 
-    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(10))
-    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 unable to send a needed message to a downstream subscriber"\n'
+    _put_ac_instance(client, monkeypatch, adm_instance_good)
 
-    monkeypatch.setattr("rmr.rmr.rmr_send_msg", rmr_mocks.send_mock_generator(5))
-    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 unable to send a needed message to a downstream subscriber"\n'
+    """
+    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)
 
-def test_bad_requests(client, monkeypatch):
-    """Test bad requests"""
-    testing_helpers.patch_all(monkeypatch)
+    # delete the instance
+    _delete_instance(client)
 
-    # test a 404
-    res = client.put("/a1-p/policies/noexist", json=testing_helpers.good_payload())
-    assert res.status_code == 404
+    _verify_instance_and_status(client, adm_instance_good, "NOT IN EFFECT", True)
 
-    # bad media type
-    res = client.put("/a1-p/policies/admission_control_policy", data="notajson")
-    assert res.status_code == 415
+    # instance should be totally gone after a few seconds
+    _instance_is_gone(client)
 
-    # test a PUT body against a poliucy not expecting one
-    res = client.put("/a1-p/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 the type
+    _delete_ac_type(client)
 
 
-def test_missing_manifest(client, monkeypatch):
+def test_bad_instances(client, monkeypatch, adm_type_good):
     """
-    test that we get a 500 with an approrpiate message on a missing manifest
+    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
 
-    def f():
-        raise exceptions.MissingManifest()
+    # test 503 handlers
 
-    monkeypatch.setattr("a1.utils.get_ric_manifest", f)
+    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()
 
-    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'
+    monkeypatch.setattr("a1.data.SDL.set", monkey_set)
 
+    res = client.put("/a1-p/policytypes/111", json=adm_type_good)
+    assert res.status_code == 503
+    res = client.put("/a1-p/policytypes/112", json=adm_type_good)
+    assert res.status_code == 503
+    res = client.put("/a1-p/policytypes/113", json=adm_type_good)
+    assert res.status_code == 503
 
-def test_missing_rmr(client, monkeypatch):
+
+def test_illegal_types(client, adm_type_good):
     """
-    test that we get a 500 with an approrpiate message on a missing rmr rmr_string
+    Test illegal types
     """
-    testing_helpers.patch_all(monkeypatch, nonexisting_rmr=True)
-    res = client.put("/a1-p/policies/admission_control_policy", json=testing_helpers.good_payload())
-    assert res.status_code == 500
-    assert res.data == b'"A1 does not have a mapping for the desired rmr string. report this!"\n'
+    res = client.put("/a1-p/policytypes/0", json=adm_type_good)
+    assert res.status_code == 400
+    res = client.put("/a1-p/policytypes/2147483648", json=adm_type_good)
+    assert res.status_code == 400
 
 
 def test_healthcheck(client):
@@ -234,3 +387,8 @@ def test_healthcheck(client):
     """
     res = client.get("/a1-p/healthcheck")
     assert res.status_code == 200
+
+
+def teardown_module():
+    """module teardown"""
+    a1rmr.stop_rmr_thread()