Skip to content
Snippets Groups Projects
tests.py 38.1 KiB
Newer Older
Nikolay Stanchev's avatar
Nikolay Stanchev committed
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
//      Created By :            Nikolay Stanchev
//      Created Date :          09-07-2018
//      Created for Project :   FLAME
"""

from json import dumps
import pytest
from pyramid import testing
from clmcservice.graphapi.views import GraphAPI
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound


graph_1_id = None
graph_2_id = None


class TestGraphAPI(object):
    """
    A pytest-implementation test for the Graph API endpoints.
    """

    @pytest.fixture(autouse=True)
    def app_config(self):
        """
        A fixture to implement setUp/tearDown functionality for all tests by initializing configuration structure for the web service and db connection
        """

        self.registry = testing.setUp()
        self.registry.add_settings({"neo4j_host": "localhost", "neo4j_password": "admin", "influx_host": "localhost", "influx_port": 8086, "network_bandwidth": 104857600})
Nikolay Stanchev's avatar
Nikolay Stanchev committed

        yield

        testing.tearDown()

    @pytest.mark.parametrize("body, from_timestamp, to_timestamp, error_msg", [
        (None, None, None, "A bad request error must have been raised in case of missing request body."),
        ('{}', 12341412, 1234897, "A bad request error must have been raised in case of invalid request body."),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfci"}', 12341412, 1234897, "A bad request error must have been raised in case of invalid request body."),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": "{invalid_json}"}', 1528386860, 1528389860, "A bad request error must have been raised in case of invalid request body."),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": ["nginx", "minio"]}', 1528386860, 1528389860, "A bad request error must have been raised in case of invalid request body."),
        ('"service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}',
         1528386860, 1528389860, "A bad request error must have been raised in case of missing service function chain value in the request body"),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfcinstance", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}',
         1528386860, 1528389860, "A bad request error must have been raised in case of invalid sfci ID in the request body"),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}',
         "not a timestamp", "not a timestamp", "A bad request error must have been raised in case of invalid URL parameters."),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}',
         None, "not a timestamp", "A bad request error must have been raised in case of invalid URL parameters."),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}',
         2131212, None, "A bad request error must have been raised in case of invalid URL parameters."),
        ('"service_function_chain": "sfc", "service_function_chain_instance": "sfc_1", "service_functions": {"nginx": {"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)", "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size"}}}',
         2131212, 2131212, "A bad request error must have been raised in case of a non-existing database."),
    ])
    def test_build_error_handling(self, body, from_timestamp, to_timestamp, error_msg):
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        """
        Tests the error handling of the graph build API endpoint by passing erroneous input and confirming an HTTPBadRequest was returned.
Nikolay Stanchev's avatar
Nikolay Stanchev committed

        :param body: body of the request to test
        :param from_timestamp: the 'from' URL param
        :param to_timestamp: the 'to' URL param
        :param error_msg: the error message to pass in case of an error not being properly handled by the API endpoint (in other words, a test failure)
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        """
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request = testing.DummyRequest()
        if body is not None:
            request.body = body
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request.body = request.body.encode(request.charset)
        if from_timestamp is not None:
            request.params["from"] = from_timestamp
        if to_timestamp is not None:
            request.params["to"] = to_timestamp
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        error_raised = False
        try:
            GraphAPI(request).build_temporal_graph()
        except HTTPBadRequest:
            error_raised = True
        assert error_raised, error_msg
Nikolay Stanchev's avatar
Nikolay Stanchev committed

    def test_build(self, db_testing_data):
        """
        Tests the graph build API endpoint - it makes 2 API calls and checks that the expected graph was created (the influx data that's being used is reported to InfluxDB in the conftest file)
        :param db_testing_data: pair of time stamps - the from-to range of the generated influx test data, test database name and the graph db client object (this is a fixture from conftest)
        global graph_1_id, graph_2_id  # these variables are used to store the ID of the graphs that were created during the execution of this test method; they are reused later when testing the delete method
        from_timestamp, to_timestamp, graph_db = db_testing_data
        ue_nodes = set([node["name"] for node in graph_db.nodes.match("UserEquipment")])
        assert ue_nodes == set("ue" + str(i) for i in [2, 3, 6]), "UE nodes have not been created"
        dc_nodes = set([node["name"] for node in graph_db.nodes.match("Cluster")])
        assert dc_nodes == set("DC" + str(i) for i in range(1, 7)), "Compute nodes must have been created by the db_testing_data fixture"

        # test with invalid URL parameters naming
        service_functions = dict(nginx={"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)",
                                        "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"},
                                 minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)",
                                        "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"},
                                 apache={"measurement_name": "apache", "response_time_field": "mean(avg_processing_time)",
                                         "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"})
        body = dumps(dict(service_function_chain="sfc", service_function_chain_instance="sfc_1", service_functions=service_functions))
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request = testing.DummyRequest()
        request.body = body.encode(request.charset)
        with pytest.raises(HTTPBadRequest):
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            GraphAPI(request).build_temporal_graph()

        # Create a valid build request and send it to the API endpoint
        service_functions = dict(nginx={"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)",
                                        "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"},
                                 minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)",
                                        "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"})
        build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_premium", service_functions=service_functions)
        build_json_body["from"] = from_timestamp
        build_json_body["to"] = to_timestamp
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        body = dumps(build_json_body)
        request = testing.DummyRequest()
        request.body = body.encode(request.charset)
        response = GraphAPI(request).build_temporal_graph()
        graph_subresponse = response.pop("graph")
        # remove the "from" and "to" keys, these will be returned in the graph_subresponse
        build_json_body.pop("from")
        build_json_body.pop("to")
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        assert response == build_json_body, "Response must contain the request body"
        assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response."
        assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9  # timestamp returned in nanoseconds
        assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9  # timestamp returned in nanoseconds
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request_id = graph_subresponse["uuid"]
        graph_1_id = request_id

        # check that the appropriate nodes have been created
        sfp_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunctionPackage")])
        assert sfp_names == {"nginx", "minio"}, "The graph must contain 2 service function packages - nginx and minio"
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        sf_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunction")])
        assert sf_names == {"nginx_1", "minio_1"}, "The graph must contain 2 service functions - nginx_1 and minio_1"
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        endpoints = set([node["name"] for node in graph_db.nodes.match("Endpoint", uuid=request_id)])
        assert endpoints == {"minio_1_ep1", "nginx_1_ep1", "nginx_1_ep2"}, "The graph must contain 3 endpoints - minio_1_ep1, nginx_1_ep1, nginx_1_ep2"
        sfci_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChainInstance")])
        assert sfci_names == {"test_sfc_premium"}, "The graph must contain 1 service function chain instance - test_sfc_premium"
        sfc_names = set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChain")])
        assert sfc_names == {"test_sfc"}, "The graph must contain 1 service function chain - test_sfc"
        reference_node = graph_db.nodes.match("Reference", uuid=request_id, sfci="test_sfc_premium", sfc="test_sfc").first()
        assert reference_node is not None and reference_node["from"] == from_timestamp * 10**9 and reference_node["to"] == to_timestamp * 10**9, "Reference node must have been created"
Nikolay Stanchev's avatar
Nikolay Stanchev committed

        # check the appropriate edges have been created
        self.check_exist_relationship(
            (
                ("minio_1_ep1", "Endpoint", "DC4", "Cluster", "hostedBy"),
                ("nginx_1_ep1", "Endpoint", "DC4", "Cluster", "hostedBy"),
                ("nginx_1_ep2", "Endpoint", "DC6", "Cluster", "hostedBy"),
                ("minio_1", "ServiceFunction", "minio_1_ep1", "Endpoint", "realisedBy"),
                ("nginx_1", "ServiceFunction", "nginx_1_ep1", "Endpoint", "realisedBy"),
                ("nginx_1", "ServiceFunction", "nginx_1_ep2", "Endpoint", "realisedBy"),
                ("minio_1", "ServiceFunction", "minio", "ServiceFunctionPackage", "instanceOf"),
                ("nginx_1", "ServiceFunction", "test_sfc_premium", "ServiceFunctionChainInstance", "utilizedBy"),
                ("minio_1", "ServiceFunction", "test_sfc_premium", "ServiceFunctionChainInstance", "utilizedBy"),
                ("nginx", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"),
                ("minio", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"),
                ("test_sfc_premium", "ServiceFunctionChainInstance", "test_sfc", "ServiceFunctionChain", "instanceOf"),
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            ), graph_db, request_id
        )

        # check endpoint nodes have the correct properties
        for endpoint, response_time, request_size, response_size in (("minio_1_ep1", 9, 5760, 2033), ("nginx_1_ep1", 18.2, 2260, 9660), ("nginx_1_ep2", 22.2, 35600, 6420)):
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            endpoint_node = graph_db.nodes.match("Endpoint", name=endpoint, uuid=request_id).first()
            assert endpoint_node["response_time"] == response_time, "Wrong response time property of endpoint node"
            # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing
            assert endpoint_node["request_size"] == pytest.approx(request_size, 1), "Wrong request size attribute of endpoint node"
            assert endpoint_node["response_size"] == pytest.approx(response_size, 1), "Wrong response size attribute of endpoint node"
        # send a new request for a new service function chain instance and check the new subgraph has been created
        service_functions = dict(minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)",
                                        "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"},
                                 apache={"measurement_name": "apache", "response_time_field": "mean(avg_processing_time)",
                                         "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"})
        build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_non_premium", service_functions=service_functions)
        build_json_body["from"] = from_timestamp
        build_json_body["to"] = to_timestamp
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        body = dumps(build_json_body)
        request = testing.DummyRequest()
        request.body = body.encode(request.charset)
        response = GraphAPI(request).build_temporal_graph()
        graph_subresponse = response.pop("graph")
        # remove the "from" and "to" keys, these will be returned in the graph_subresponse
        build_json_body.pop("from")
        build_json_body.pop("to")
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        assert response == build_json_body, "Response must contain the request body"
        assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response."
        assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9  # timestamp returned in nanoseconds
        assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9  # timestamp returned in nanoseconds
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request_id = graph_subresponse["uuid"]
        graph_2_id = request_id

        # check the new nodes have been created
        assert graph_db.nodes.match("ServiceFunctionPackage", name="apache").first() is not None, "Service function package apache must have been added to the graph"
        for sf in ("apache_1", "minio_2"):
            assert graph_db.nodes.match("ServiceFunction", name=sf).first() is not None, "Service function {0} must have been added to the graph".format(sf)
Nikolay Stanchev's avatar
Nikolay Stanchev committed

        for ep in ("minio_2_ep1", "apache_1_ep1"):
            assert graph_db.nodes.match("Endpoint", name=ep, uuid=request_id).first() is not None, "Endpoint {0} must have been added to the graph".format(ep)

        assert graph_db.nodes.match("ServiceFunctionChainInstance", name="test_sfc_non_premium").first() is not None, "Service function chain instance test_sfc_non_premium must have been added to the graph"
        assert graph_db.nodes.match("ServiceFunctionChain", name="test_sfc").first() is not None, "Service function chain test_sfc must have been added to the graph"
        reference_node = graph_db.nodes.match("Reference", uuid=request_id, sfci="test_sfc_non_premium", sfc="test_sfc").first()
        assert reference_node is not None and reference_node["from"] == from_timestamp * 10**9 and reference_node["to"] == to_timestamp * 10**9, "Reference node must have been created"

Nikolay Stanchev's avatar
Nikolay Stanchev committed
        # check the appropriate edges have been created
        self.check_exist_relationship(
            (
                ("minio_2_ep1", "Endpoint", "DC5", "Cluster", "hostedBy"),
                ("apache_1_ep1", "Endpoint", "DC5", "Cluster", "hostedBy"),
                ("minio_2", "ServiceFunction", "minio_2_ep1", "Endpoint", "realisedBy"),
                ("apache_1", "ServiceFunction", "apache_1_ep1", "Endpoint", "realisedBy"),
                ("minio_2", "ServiceFunction", "minio", "ServiceFunctionPackage", "instanceOf"),
                ("apache_1", "ServiceFunction", "apache", "ServiceFunctionPackage", "instanceOf"),
                ("minio_2", "ServiceFunction", "test_sfc_non_premium", "ServiceFunctionChainInstance", "utilizedBy"),
                ("apache_1", "ServiceFunction", "test_sfc_non_premium", "ServiceFunctionChainInstance", "utilizedBy"),
                ("minio", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"),
                ("apache", "ServiceFunctionPackage", "test_sfc", "ServiceFunctionChain", "utilizedBy"),
                ("test_sfc_non_premium", "ServiceFunctionChainInstance", "test_sfc", "ServiceFunctionChain", "instanceOf")
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            ), graph_db, request_id
        )

        # check endpoint nodes have the correct properties
        for endpoint, response_time, request_size, response_size in (("minio_2_ep1", 7, 2998, 3610), ("apache_1_ep1", 17.6, 1480, 7860)):
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            endpoint_node = graph_db.nodes.match("Endpoint", name=endpoint, uuid=request_id).first()
            assert endpoint_node["response_time"] == response_time, "Wrong response time property of endpoint node"
            # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing
            assert endpoint_node["request_size"] == pytest.approx(request_size, 1), "Wrong request size attribute of endpoint node"
            assert endpoint_node["response_size"] == pytest.approx(response_size, 1), "Wrong response size attribute of endpoint node"
Nikolay Stanchev's avatar
Nikolay Stanchev committed

    def test_delete(self, db_testing_data):
        """
        Tests the delete API endpoint of the Graph API - the test depends on the build test to have been passed successfully so that graph_1_id and graph_2_id have been set
        :param db_testing_data: pair of time stamps - the from-to range of the generated influx test data, test database name and the graph db client object (this is a fixture from conftest)
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        """

        global graph_1_id, graph_2_id

        from_timestamp, to_timestamp, graph_db = db_testing_data
Nikolay Stanchev's avatar
Nikolay Stanchev committed

        request = testing.DummyRequest()
        request.matchdict["graph_id"] = "invalid_graph_id"
        error_raised = False
        try:
            GraphAPI(request).delete_temporal_graph()
        except HTTPNotFound:
            error_raised = True
        assert error_raised, "HTTP Not Found error must be raised in case of unrecognized subgraph ID"

        # delete the graph associated with graph_1_id
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request = testing.DummyRequest()
        request.matchdict["graph_id"] = graph_1_id
        response = GraphAPI(request).delete_temporal_graph()
        assert response == {"uuid": graph_1_id, "deleted": 4}, "Incorrect response when deleting temporal graph"
        # delete the graph associated with graph_2_id
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request = testing.DummyRequest()
        request.matchdict["graph_id"] = graph_2_id
        response = GraphAPI(request).delete_temporal_graph()
        assert response == {"uuid": graph_2_id, "deleted": 3}, "Incorrect response when deleting temporal graph"
Nikolay Stanchev's avatar
Nikolay Stanchev committed

        assert len(graph_db.nodes.match("Endpoint")) == 0, "All endpoint nodes should have been deleted"
        assert set([node["name"] for node in graph_db.nodes.match("Cluster")]) == set(["DC" + str(i) for i in range(1, 7)]), "Compute nodes must not be deleted"
        assert set([node["name"] for node in graph_db.nodes.match("ServiceFunction")]) == {"nginx_1", "apache_1", "minio_1", "minio_2"}, "Service functions must not be deleted."
        assert set([node["name"] for node in graph_db.nodes.match("ServiceFunctionPackage")]) == {"nginx", "minio", "apache"}, "Service function packages must not be deleted"
        assert set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChainInstance")]) == {"test_sfc_premium", "test_sfc_non_premium"}, "Service function chain instances must not be deleted"
        assert set([node["name"] for node in graph_db.nodes.match("ServiceFunctionChain")]) == {"test_sfc"}, "Service function chains must not be deleted"
    @pytest.mark.parametrize("graph_id, endpoint, startpoint, error_type, error_msg", [
        ('e8cd4768-47dd-48cd-9c74-7f8926ddbad8', None, None, HTTPBadRequest, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"),
        ('e8cd4768-47dd-48cd-9c74-7f8926ddbad8', None, "nginx", HTTPBadRequest, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"),
        ('e8cd4768-47dd-48cd-9c74-7f8926ddbad8', "nginx_1_ep1", None, HTTPBadRequest, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"),
        ('random-uuid', "nginx_1_ep1", "nginx", HTTPNotFound, "HTTP Not Found error must be thrown for an endpoint node with incorrect request ID"),
        ('random-uuid', "minio_1_ep1", "minio", HTTPNotFound, "HTTP Not Found error must be thrown for an endpoint node with incorrect request ID"),
    ])
    def test_rtt_error_handling(self, graph_id, endpoint, startpoint, error_type, error_msg):
        Tests the error handling of the graph round trip time API endpoint - achieved by sending erroneous input in the request and verifying the appropriate error type has been returned.

        :param graph_id: the UUID of the subgraph
        :param endpoint: endpoint ID
        :param startpoint: the start node ID
        :param error_type: error type to expect as a response
        :param error_msg: error message in case of a test failure
        """

        request = testing.DummyRequest()
        request.matchdict["graph_id"] = graph_id
        if endpoint is not None:
            request.params["endpoint"] = endpoint
        if startpoint is not None:
            request.params["startpoint"] = startpoint
        error_raised = False
        try:
            GraphAPI(request).run_rtt_query()
        except error_type:
            error_raised = True
        assert error_raised, error_msg

Nikolay Stanchev's avatar
Nikolay Stanchev committed
    def test_rtt(self, db_testing_data):
        """
        Tests the rtt API endpoint of the Graph API.
        :param db_testing_data: pair of time stamps - the from-to range of the generated influx test data, test database name and the graph db client object (this is a fixture from conftest)
        from_timestamp, to_timestamp, graph_db = db_testing_data
        # create a graph to use for RTT test by using the build API endpoint
        service_functions = dict(nginx={"measurement_name": "nginx", "response_time_field": "mean(avg_processing_time)",
                                        "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"},
                                 minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)",
                                        "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"})
        build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_premium", service_functions=service_functions)
        build_json_body["from"] = from_timestamp
        build_json_body["to"] = to_timestamp
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        body = dumps(build_json_body)
        request = testing.DummyRequest()
        request.body = body.encode(request.charset)
        response = GraphAPI(request).build_temporal_graph()
        graph_subresponse = response.pop("graph")
        # remove the "from" and "to" keys, these will be returned in the graph_subresponse
        build_json_body.pop("from")
        build_json_body.pop("to")
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        assert response == build_json_body, "Response must contain the request body"
        assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response."
        assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9  # timestamp returned in nanoseconds
        assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9  # timestamp returned in nanoseconds
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request_id = graph_subresponse["uuid"]

        # test some more error case handling of the RTT API endpoint
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request = testing.DummyRequest()
        request.matchdict["graph_id"] = request_id
        request.params["endpoint"] = "nginx_1_ep1"
        request.params["compute"] = "DC1"
        error_raised = False
        try:
            GraphAPI(request).run_rtt_query()
        except HTTPBadRequest:
            error_raised = True
        assert error_raised, "HTTP Bad Request must be thrown in case of missing or invalid url parameters"

        request = testing.DummyRequest()
        request.matchdict["graph_id"] = request_id
        request.params["endpoint"] = "nginx_1_ep1"
        request.params["startpoint"] = "DC0"
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        error_raised = False
        try:
            GraphAPI(request).run_rtt_query()
        except HTTPNotFound:
            error_raised = True
        assert error_raised, "HTTP Not Found error must be thrown for non existing compute node"

        request = testing.DummyRequest()
        request.matchdict["graph_id"] = request_id
        request.params["endpoint"] = "apache_1_ep1"
        request.params["startpoint"] = "DC1"
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        error_raised = False
        try:
            GraphAPI(request).run_rtt_query()
        except HTTPNotFound:
            error_raised = True
        assert error_raised, "HTTP Not Found error must be thrown for a non existing endpoint"

        # go through the set of input/output (expected) parameters and assert actual results match with expected ones
        for startpoint, endpoint, forward_latencies, reverse_latencies, response_time, request_size, response_size, rtt, global_tags in (
            ("DC6", "nginx_1_ep2", [], [], 22.2, 35600, 6420, 22.2, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("127.0.0.6", "nginx_1_ep2", [0], [0], 22.2, 35600, 6420, 22.2, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("ue6", "nginx_1_ep2", [0, 0], [0, 0], 22.2, 35600, 6420, 22.2, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("DC2", "nginx_1_ep2", [0, 7.5, 15, 4.5, 0], [0, 4.5, 15, 7.5, 0], 22.2, 35600, 6420, 78, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("127.0.0.2", "nginx_1_ep2", [7.5, 15, 4.5, 0], [0, 4.5, 15, 7.5], 22.2, 35600, 6420, 78, {"flame_location": "DC6", "flame_sfe": "nginx_1_ep2", "flame_server": "DC6", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("DC3", "nginx_1_ep1", [0, 12.5, 0], [0, 12.5, 0], 18.2, 2260, 9660, 38, {"flame_location": "DC4", "flame_sfe": "nginx_1_ep1", "flame_server": "DC4", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("127.0.0.3", "nginx_1_ep1", [12.5, 0], [0, 12.5], 18.2, 2260, 9660, 38, {"flame_location": "DC4", "flame_sfe": "nginx_1_ep1", "flame_server": "DC4", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"}),
            ("ue3", "nginx_1_ep1", [0, 12.5, 0], [0, 12.5, 0], 18.2, 2260, 9660, 38, {"flame_location": "DC4", "flame_sfe": "nginx_1_ep1", "flame_server": "DC4", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_premium", "flame_sfp": "nginx", "flame_sf": "nginx_1"})
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        ):
            request = testing.DummyRequest()
            request.matchdict["graph_id"] = request_id
            request.params["endpoint"] = endpoint
            request.params["startpoint"] = startpoint
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            response = GraphAPI(request).run_rtt_query()
            # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing
            assert response.pop("round_trip_time") == pytest.approx(rtt, 1), "Incorrect RTT response"
            assert response == {"forward_latencies": forward_latencies, "reverse_latencies": reverse_latencies, "total_forward_latency": sum(forward_latencies), "total_reverse_latency": sum(reverse_latencies),
                                "bandwidth": 104857600, "response_time": response_time, "local_tags": {"traffic_source": startpoint}, "global_tags": global_tags,
                                "request_size": request_size, "response_size": response_size}, "Incorrect RTT response"
        # send a new request for a new service function chain to create a second subgraph to test
        service_functions = dict(minio={"measurement_name": "minio_http", "response_time_field": "mean(total_processing_time)/mean(total_requests_count)",
                                        "request_size_field": "mean(total_requests_size)/mean(total_requests_count)", "response_size_field": "mean(total_response_size)/mean(total_requests_count)"},
                                 apache={"measurement_name": "apache", "response_time_field": "mean(avg_processing_time)",
                                         "request_size_field": "mean(avg_request_size)", "response_size_field": "mean(avg_response_size)"})
        build_json_body = dict(service_function_chain="test_sfc", service_function_chain_instance="test_sfc_non_premium", service_functions=service_functions)
        build_json_body["from"] = from_timestamp
        build_json_body["to"] = to_timestamp
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        body = dumps(build_json_body)
        request = testing.DummyRequest()
        request.body = body.encode(request.charset)
        response = GraphAPI(request).build_temporal_graph()
        graph_subresponse = response.pop("graph")
        # remove the "from" and "to" keys, these will be returned in the graph_subresponse
        build_json_body.pop("from")
        build_json_body.pop("to")
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        assert response == build_json_body, "Response must contain the request body"
        assert graph_subresponse.get("uuid") is not None, "Request UUID must be attached to the response."
        assert graph_subresponse["time_range"]["from"] == from_timestamp * 10**9  # timestamp returned in nanoseconds
        assert graph_subresponse["time_range"]["to"] == to_timestamp * 10**9  # timestamp returned in nanoseconds
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        request_id = graph_subresponse["uuid"]

        # go through the set of input/output (expected) parameters and assert actual results match with expected ones
        for startpoint, endpoint, forward_latencies, reverse_latencies, response_time, request_size, response_size, rtt, global_tags in (
            ("DC5", "apache_1_ep1", [], [], 17.6, 1480, 7860, 17.6, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}),
            ("127.0.0.5", "apache_1_ep1", [0], [0], 17.6, 1480, 7860, 17.6, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}),
            ("DC5", "minio_2_ep1", [], [], 7, 2998, 3610, 7, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}),
            ("127.0.0.5", "minio_2_ep1", [0], [0], 7, 2998, 3610, 7, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}),
            ("DC3", "apache_1_ep1", [0, 9, 15, 0], [0, 15, 9, 0], 17.6, 1480, 7860, 64, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}),
            ("127.0.0.3", "apache_1_ep1", [9, 15, 0], [0, 15, 9], 17.6, 1480, 7860, 64, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}),
            ("ue3", "apache_1_ep1", [0, 9, 15, 0], [0, 15, 9, 0], 17.6, 1480, 7860, 64, {"flame_location": "DC5", "flame_sfe": "apache_1_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "apache", "flame_sf": "apache_1"}),
            ("DC2", "minio_2_ep1", [0, 7.5, 15, 0], [0, 15, 7.5, 0], 7, 2998, 3610, 53, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}),
            ("127.0.0.2", "minio_2_ep1", [7.5, 15, 0], [0, 15, 7.5], 7, 2998, 3610, 53, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"}),
            ("ue2", "minio_2_ep1", [0, 7.5, 15, 0], [0, 15, 7.5, 0], 7, 2998, 3610, 53, {"flame_location": "DC5", "flame_sfe": "minio_2_ep1", "flame_server": "DC5", "flame_sfc": "test_sfc", "flame_sfci": "test_sfc_non_premium", "flame_sfp": "minio", "flame_sf": "minio_2"})
Nikolay Stanchev's avatar
Nikolay Stanchev committed
        ):
            request = testing.DummyRequest()
            request.matchdict["graph_id"] = request_id
            request.params["endpoint"] = endpoint
            request.params["startpoint"] = startpoint
Nikolay Stanchev's avatar
Nikolay Stanchev committed
            response = GraphAPI(request).run_rtt_query()
            # approximation is used to avoid long float numbers retrieved from influx, the test case ensures the results are different enough so that approximation of +-1 is good enough for testing
            assert response.pop("request_size") == pytest.approx(request_size, 1), "Incorrect RTT response"
            assert response.pop("response_size") == pytest.approx(response_size, 1), "Incorrect RTT response"
            assert response.pop("round_trip_time") == pytest.approx(rtt, 1), "Incorrect RTT response"
            assert response == {"forward_latencies": forward_latencies, "reverse_latencies": reverse_latencies, "total_forward_latency": sum(forward_latencies), "total_reverse_latency": sum(reverse_latencies),
                                "bandwidth": 104857600, "response_time": response_time, "local_tags": {"traffic_source": startpoint}, "global_tags": global_tags}, "Incorrect RTT response"
    def test_delete_network_graph(self):
        """
        Tests the delete network graph functionality.
        """

        request = testing.DummyRequest()
        response = GraphAPI(request).delete_network_topology()

        assert response == {"deleted_switches_count": 6, "deleted_clusters_count": 6, "deleted_ues_count": 3}
Nikolay Stanchev's avatar
Nikolay Stanchev committed
    @staticmethod
    def check_exist_relationship(relationships_tuple, graph, uuid):
        Iterates through a tuple of relationships and checks that each of those exists - a utility method to be reused for testing.

        :param relationships_tuple: the tuple to iterate
        :param graph: the graph object
        :param uuid: the uuid of the request
        """

Nikolay Stanchev's avatar
Nikolay Stanchev committed
        for relationship in relationships_tuple:
            from_node_name, from_node_type, to_node_name, to_node_type, relationship_type = relationship
            if from_node_type == "Endpoint":
                from_node = graph.nodes.match(from_node_type, name=from_node_name, uuid=uuid).first()
            else:
                from_node = graph.nodes.match(from_node_type, name=from_node_name).first()
            assert from_node is not None  # IMPORTANT, assert the from_node exists, otherwise the py2neo RelationshipMatcher object assumes you are looking for any node (instead of raising an error)
Nikolay Stanchev's avatar
Nikolay Stanchev committed

            if to_node_type == "Endpoint":
                to_node = graph.nodes.match(to_node_type, name=to_node_name, uuid=uuid).first()
            else:
                to_node = graph.nodes.match(to_node_type, name=to_node_name).first()
            assert to_node is not None  # IMPORTANT, assert the from_node exists, otherwise the py2neo RelationshipMatcher object assumes you are looking for any node (instead of raising an error)
Nikolay Stanchev's avatar
Nikolay Stanchev committed

            assert graph.relationships.match(nodes=(from_node, to_node), r_type=relationship_type).first() is not None, "Graph is missing a required relationship"