You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sh...@apache.org on 2023/03/28 16:23:40 UTC

[trafficcontrol] branch master updated: Various improvements to the API contract tests (#7419)

This is an automated email from the ASF dual-hosted git repository.

shamrick pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 67cf92d665 Various improvements to the API contract tests (#7419)
67cf92d665 is described below

commit 67cf92d665da915b0727145cb718d6fddd3ae1d1
Author: ocket8888 <oc...@apache.org>
AuthorDate: Tue Mar 28 10:23:32 2023 -0600

    Various improvements to the API contract tests (#7419)
    
    * Get rid of removed Pylint config option
    
    * Use real indentation
    
    * foo
    
    * Slight formatting changes
    
    * Greatly improve type safety in conftest main script
    
    * fix use of reserved short option
    
    * Fix URLs not allowed to omit port number
    
    * Fix using deprecated log method
    
    * add context to URL parsing errors
    
    * Make prerequisite data file location configurable
    
    * don't return duplicate data in the prerequisite fixture
    
    * Make "cdns" an array
    
    It's plural, so I assume that was the original intent
    
    * Fix typechecking issues
    
    * Fix linter errors
    
    all but one were about lines being too long
---
 traffic_control/clients/python/pylint.rc           |   7 -
 traffic_ops/testing/api_contract/v4/conftest.py    | 393 +++++++++++++++------
 .../testing/api_contract/v4/prerequisite_data.json |  14 +-
 traffic_ops/testing/api_contract/v4/test_cdns.py   | 152 +++++---
 4 files changed, 399 insertions(+), 167 deletions(-)

diff --git a/traffic_control/clients/python/pylint.rc b/traffic_control/clients/python/pylint.rc
index 77d93556e1..aadd4fe56e 100644
--- a/traffic_control/clients/python/pylint.rc
+++ b/traffic_control/clients/python/pylint.rc
@@ -360,13 +360,6 @@ max-line-length=100
 # Maximum number of lines in a module.
 max-module-lines=1000
 
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
-			   dict-separator
-
 # Allow the body of a class to be on the same line as the declaration if body
 # contains single statement.
 single-line-class-stmt=no
diff --git a/traffic_ops/testing/api_contract/v4/conftest.py b/traffic_ops/testing/api_contract/v4/conftest.py
index 684089b6b0..bc2bb16f6f 100644
--- a/traffic_ops/testing/api_contract/v4/conftest.py
+++ b/traffic_ops/testing/api_contract/v4/conftest.py
@@ -12,132 +12,327 @@
 # limitations under the License.
 #
 
-"""This module is used to create a Traffic Ops session 
-and to store prerequisite data for endpoints."""
+"""
+This module is used to create a Traffic Ops session and to store prerequisite
+data for endpoints.
+"""
+
 import json
 import logging
 import sys
 import os
 from random import randint
-from typing import NamedTuple, Optional
+from typing import NamedTuple, Union, Optional, TypeAlias
 from urllib.parse import urlparse
+
 import pytest
 import requests
+
 from trafficops.tosession import TOSession
 from trafficops.restapi import OperationError
 
-
 # Create and configure logger
 logger = logging.getLogger()
 
+JSONData: TypeAlias = Union[dict[str, object], list[object], bool, int, float, str | None]
+JSONData.__doc__ = """An alias for the kinds of data that JSON can encode."""
+
+class APIVersion(NamedTuple):
+	"""Represents an API version."""
+	major: int
+	minor: int
+
+	@staticmethod
+	def from_string(ver_str: str) -> "APIVersion":
+		"""
+		Instantiates a new version from a string.
+
+		>>> APIVersion.from_string("4.0")
+		APIVersion(major=4, minor=0)
+		>>> try:
+		... 	APIVersion("not a version string")
+		... except ValueError:
+		... 	print("whoops")
+		...
+		whoops
+		>>>
+		>>> try:
+		... 	APIVersion("4.Q")
+		... except ValueError:
+		... 	print("whoops")
+		...
+		whoops
+		"""
+		parts = ver_str.split(".", 1)
+		if len(parts) != 2:
+			raise ValueError("invalid version; must be of the form '{{major}}.{{minor}}'")
+		return APIVersion(int(parts[0]), int(parts[1]))
+
+	def __str__(self) -> str:
+		"""
+		Coalesces the version to a string.
+
+		>>> print(APIVersion(4, 1))
+		4.1
+		"""
+		return f"{self.major}.{self.minor}"
+
+APIVersion.major.__doc__ = """The API's major version number."""
+APIVersion.major.__doc__ = """The API's minor version number."""
 
 class ArgsType(NamedTuple):
-    """Represents the arguments needed to create Traffic Ops session.
-
-    Attributes:
-        user (str): The username used for authentication.
-        password (str): The password used for authentication.
-        url (str): The URL of the environment.
-        port (int): The port number to use for session.
-        api_version (float): The version number of the API to use.
-    """
-    user: str
-    password: str
-    url: str
-    port: int
-    api_version: float
+	"""Represents the configuration needed to create Traffic Ops session."""
+	user: str
+	password: str
+	url: str
+	port: int
+	api_version: APIVersion
 
+	def __str__(self) -> str:
+		"""
+		Formats the configuration as a string. Omits password and extraneous
+		properties.
+
+		>>> print(ArgsType("user", "password", "url", 420, APIVersion(4, 0)))
+		User: 'user', URL: 'url'
+		"""
+		return f"User: '{self.user}', URL: '{self.url}'"
+
+
+ArgsType.user.__doc__ = """The username used for authentication."""
+ArgsType.password.__doc__ = """The password used for authentication."""
+ArgsType.url.__doc__ = """The URL of the environment."""
+ArgsType.port.__doc__ = """The port number on which to connect to Traffic Ops."""
+ArgsType.api_version.__doc__ = """The version number of the API to use."""
 
 def pytest_addoption(parser: pytest.Parser) -> None:
-    """Passing in Traffic Ops arguments [Username, Password, Url and Hostname] from command line.
-    :param parser: Parser to parse command line arguments
-    """
-    parser.addoption(
-        "--to-user", action="store", help="User name for Traffic Ops Session."
-    )
-    parser.addoption(
-        "--to-password", action="store", help="Password for Traffic Ops Session."
-    )
-    parser.addoption(
-        "--to-url", action="store", help="Traffic Ops URL."
-    )
+	"""
+	Parses the Traffic Ops arguments from command line.
+	:param parser: Parser to parse command line arguments.
+	"""
+	parser.addoption(
+		"--to-user", action="store", help="User name for Traffic Ops Session."
+	)
+	parser.addoption(
+		"--to-password", action="store", help="Password for Traffic Ops Session."
+	)
+	parser.addoption(
+		"--to-url", action="store", help="Traffic Ops URL."
+	)
+	parser.addoption(
+		"--config",
+		help="Path to configuration file.",
+		default=os.path.join(os.path.dirname(__file__), "to_data.json")
+	)
+	parser.addoption(
+		"--prerequisites",
+		help="Path to prerequisites file.",
+		default=os.path.join(os.path.dirname(__file__), "prerequisite_data.json")
+	)
+
+def coalesce_config(
+	arg: object | None,
+	file_key: str,
+	file_contents: dict[str, object | None] | None,
+	env_key: str
+) -> Optional[str]:
+	"""
+	Coalesces configuration retrieved from different sources into a single
+	string.
+
+	This will raise a ValueError if the type of the configuration value in the
+	parsed configuration file is not a string.
+
+	In order of descending precedence this checks the command-line argument
+	value, the configuration file value, and then the environment variable
+	value.
+
+	:param arg: The command-line argument value.
+	:param file_key: The key under which to look in the parsed JSON configuration file data.
+	:param file_contents: The parsed JSON configuration file (if one was used).
+	:param env_key: The environment variable name to look for a value if one wasn't provided elsewhere.
+	:returns: The coalesced configuration value, or 'None' if no value could be determined.
+	"""
+	if isinstance(arg, str):
+		return arg
+
+	if file_contents:
+		file_value = file_contents.get(file_key)
+		if isinstance(file_value, str):
+			return file_value
+		if file_value is not None:
+			raise ValueError(f"incorrect value; want: 'str', got: '{type(file_value)}'")
+
+	return os.environ.get(env_key)
 
+def parse_to_url(raw: str) -> tuple[APIVersion, int]:
+	"""
+	Parses the API version and port number from a raw URL string.
+
+	>>> parse_to_url("https://trafficops.example.test:420/api/5.270)
+	(APIVersion(major=5, minor=270), 420)
+	>>> parse_to_url("trafficops.example.test")
+	(APIVersion(major=4, minor=0), 443)
+	"""
+	parsed = urlparse(raw)
+	if not parsed.netloc:
+		raise ValueError("missing network location (hostname & optional port)")
+
+	if parsed.scheme and parsed.scheme.lower() != "https":
+		raise ValueError("invalid scheme; must use HTTPS")
+
+	port = 443
+	if ":" in parsed.netloc:
+		port_str = parsed.netloc.split(":")[-1]
+		try:
+			port = int(port_str)
+		except ValueError as e:
+			raise ValueError(f"invalid port number: {port_str}") from e
+
+	api_version = APIVersion(4, 0)
+	if parsed.path and parsed.path != "/":
+		ver_str = parsed.path.lstrip("/api/").split("/", 1)[0]
+		if not ver_str:
+			raise ValueError(f"invalid API path: {parsed.path} (should be e.g. '/api/4.0')")
+		api_version = APIVersion.from_string(ver_str)
+	else:
+		logging.warning("using default API version: %s", api_version)
+
+	return (api_version, port)
 
 @pytest.fixture(name="to_args")
 def to_data(pytestconfig: pytest.Config) -> ArgsType:
-    """PyTest fixture to store Traffic ops arguments passed from command line.
-    :param pytestconfig: Session-scoped fixture that returns the session's pytest.Config object
-    :returns args: Return Traffic Ops arguments
-    """
-    with open(os.path.join(os.path.dirname(__file__), "to_data.json"),
-            encoding="utf-8", mode="r") as session_file:
-        session_data = json.load(session_file)
-    to_user = pytestconfig.getoption("--to-user")
-    to_password = pytestconfig.getoption("--to-password")
-    to_url = pytestconfig.getoption("--to-url")
-    api_version = urlparse(
-        session_data.get("url")).path.strip("/").split("/")[1]
-
-    if not all([to_user, to_password, to_url]):
-        logger.info(
-            "Traffic Ops session data were not passed from Command line Args.")
-        args = ArgsType(user=session_data.get("user"), password=session_data.get(
-            "password"), url=session_data.get("url"), port=session_data.get("port"),
-            api_version=api_version)
-
-    else:
-        args = ArgsType(user=to_user, password=to_password, url=to_url,
-                        port=session_data.get("port"), api_version=api_version)
-        logger.info("Parsed Traffic ops session data from args %s", args)
-    return args
+	"""
+	PyTest fixture to store Traffic ops arguments passed from command line.
+	:param pytestconfig: Session-scoped fixture that returns the session's pytest.Config object.
+	:returns: Configuration for connecting to Traffic Ops.
+	"""
+	session_data: JSONData = None
+	cfg_path = pytestconfig.getoption("--config")
+	if isinstance(cfg_path, str):
+		try:
+			with open(cfg_path, encoding="utf-8", mode="r") as session_file:
+				session_data = json.load(session_file)
+		except (FileNotFoundError, PermissionError) as read_err:
+			raise ValueError(f"could not read configuration file at '{cfg_path}'") from read_err
+
+	if session_data is not None and not isinstance(session_data, dict):
+		raise ValueError(
+			f"invalid configuration file; expected top-level object, got: {type(session_data)}"
+		)
+
+	to_user = coalesce_config(pytestconfig.getoption("--to-user"), "user", session_data, "TO_USER")
+	if not to_user:
+		raise ValueError(
+			"Traffic Ops password is not configured - use '--to-password', the config file, or an "
+			"environment variable to do so"
+		)
+
+	to_password = coalesce_config(
+		pytestconfig.getoption("--to-password"),
+		"password",
+		session_data,
+		"TO_PASSWORD"
+	)
+
+	if not to_password:
+		raise ValueError(
+			"Traffic Ops password is not configured - use '--to-password', the config file, or an "
+			"environment variable to do so"
+		)
 
+	to_url = coalesce_config(pytestconfig.getoption("--to-url"), "url", session_data, "TO_USER")
+	if not to_url:
+		raise ValueError(
+			"Traffic Ops URL is not configured - use '--to-url', the config file, or an "
+			"environment variable to do so"
+		)
+
+	try:
+		api_version, port = parse_to_url(to_url)
+	except ValueError as e:
+		raise ValueError("invalid Traffic Ops URL") from e
+
+	return ArgsType(
+		to_user,
+		to_password,
+		to_url,
+		port,
+		api_version
+	)
 
 @pytest.fixture(name="to_session")
 def to_login(to_args: ArgsType) -> TOSession:
-    """PyTest Fixture to create a Traffic Ops session from Traffic Ops Arguments
-    passed as command line arguments in to_args fixture in conftest.
-    :param to_args: Fixture to get Traffic ops session arguments
-    :returns to_session: Return Traffic ops session
-    """
-    # Create a Traffic Ops V4 session and login
-    to_url = urlparse(to_args.url)
-    to_host = to_url.hostname
-    try:
-        to_session = TOSession(host_ip=to_host, host_port=to_args.port,
-                               api_version=to_args.api_version, ssl=True, verify_cert=False)
-        logger.info("Established Traffic Ops Session.")
-    except OperationError as error:
-        logger.debug("%s", error, exc_info=True, stack_info=True)
-        logger.error(
-            "Failure in Traffic Ops session creation. Reason: %s", error)
-        sys.exit(-1)
-
-    # Login To TO_API
-    to_session.login(to_args.user, to_args.password)
-    logger.info("Successfully logged into Traffic Ops.")
-    return to_session
+	"""
+	PyTest Fixture to create a Traffic Ops session from Traffic Ops Arguments
+	passed as command line arguments in to_args fixture in conftest.
+
+	:param to_args: Fixture to get Traffic ops session arguments.
+	:returns: An authenticated Traffic Ops session.
+	"""
+	# Create a Traffic Ops V4 session and login
+	to_url = urlparse(to_args.url)
+	to_host = to_url.hostname
+	try:
+		to_session = TOSession(
+			host_ip=to_host,
+			host_port=to_args.port,
+			api_version=str(to_args.api_version),
+			ssl=True,
+			verify_cert=False
+		)
+		logger.info("Established Traffic Ops Session.")
+	except OperationError as error:
+		logger.debug("%s", error, exc_info=True, stack_info=True)
+		logger.error("Failure in Traffic Ops session creation. Reason: %s", error)
+		sys.exit(-1)
+
+	# Login To TO_API
+	to_session.login(to_args.user, to_args.password)
+	logger.info("Successfully logged into Traffic Ops.")
+	return to_session
 
 
 @pytest.fixture()
-def cdn_post_data(to_session: TOSession, cdn_prereq_data:
-                  object) -> Optional[list[dict[str, str] | requests.Response]]:
-    """PyTest Fixture to create POST data for cdns endpoint.
-    :param to_session: Fixture to get Traffic ops session 
-    :param get_cdn_data: Fixture to get cdn data from a prereq file
-    :returns prerequisite_data: Returns sample Post data and actual api response
-    """
-
-    # Return new post data and post response from cdns POST request
-    cdn_prereq_data["name"] = cdn_prereq_data["name"][:4]+str(randint(0, 1000))
-    cdn_prereq_data["domainName"] = cdn_prereq_data["domainName"][:5] + \
-        str(randint(0, 1000))
-    logger.info("New cdn data to hit POST method %s", cdn_prereq_data)
-    # Hitting cdns POST methed
-    response = to_session.create_cdn(data=cdn_prereq_data)
-    prerequisite_data = None
-    try:
-        cdn_response = response[0]
-        prerequisite_data = [cdn_prereq_data, cdn_response]
-    except IndexError:
-        logger.error("No CDN response data from cdns POST request.")
-    return prerequisite_data
+def cdn_post_data(to_session: TOSession, cdn_prereq_data: list[JSONData]) -> dict[str, object]:
+	"""
+	PyTest Fixture to create POST data for cdns endpoint.
+
+	:param to_session: Fixture to get Traffic Ops session.
+	:param get_cdn_data: Fixture to get CDN data from a prerequisites file.
+	:returns: Sample POST data and the actual API response.
+	"""
+
+	try:
+		cdn = cdn_prereq_data[0]
+	except IndexError as e:
+		raise TypeError("malformed prerequisite data; no CDNs present in 'cdns' array property") from e
+
+	if not isinstance(cdn, dict):
+		raise TypeError(f"malformed prerequisite data; CDNs must be objects, not '{type(cdn)}'")
+
+	# Return new post data and post response from cdns POST request
+	randstr = str(randint(0, 1000))
+	try:
+		name = cdn["name"]
+		if not isinstance(name, str):
+			raise TypeError(f"name must be str, not '{type(name)}'")
+		cdn["name"] = name[:4] + randstr
+		domain_name = cdn["domainName"]
+		if not isinstance(domain_name, str):
+			raise TypeError(f"domainName must be str, not '{type(domain_name)}")
+		cdn["domainName"] = domain_name[:5] + randstr
+	except KeyError as e:
+		raise TypeError(f"missing CDN property '{e.args[0]}'") from e
+
+	logger.info("New cdn data to hit POST method %s", cdn_prereq_data)
+	# Hitting cdns POST methed
+	response: tuple[JSONData, requests.Response] = to_session.create_cdn(data=cdn)
+	try:
+		resp_obj = response[0]
+		if not isinstance(resp_obj, dict):
+			raise TypeError("malformed API response; cdn is not an object")
+		return resp_obj
+	except IndexError:
+		logger.error("No CDN response data from cdns POST request.")
+		sys.exit(1)
diff --git a/traffic_ops/testing/api_contract/v4/prerequisite_data.json b/traffic_ops/testing/api_contract/v4/prerequisite_data.json
index 4939bbe284..2acf4b7801 100644
--- a/traffic_ops/testing/api_contract/v4/prerequisite_data.json
+++ b/traffic_ops/testing/api_contract/v4/prerequisite_data.json
@@ -1,9 +1,9 @@
 {
-    "cdns": {
-        "name": "test",
-        "domainName": "quest",
-        "dnssecEnabled": false,
-        "id": null,
-        "lastUpdated": null
-    }
+	"cdns": [{
+		"name": "test",
+		"domainName": "quest",
+		"dnssecEnabled": false,
+		"id": null,
+		"lastUpdated": null
+	}]
 }
diff --git a/traffic_ops/testing/api_contract/v4/test_cdns.py b/traffic_ops/testing/api_contract/v4/test_cdns.py
index 9b7e4d2491..e146ab579e 100644
--- a/traffic_ops/testing/api_contract/v4/test_cdns.py
+++ b/traffic_ops/testing/api_contract/v4/test_cdns.py
@@ -12,68 +12,112 @@
 # limitations under the License.
 #
 
-"""Api Contract Test Case for cdns endpoint."""
+"""API Contract Test Case for cdns endpoint."""
 import json
 import logging
-import os
+
 import pytest
 import requests
+
 from trafficops.tosession import TOSession
 
 # Create and configure logger
 logger = logging.getLogger()
 
+primitive = bool | int | float | str | None
 
 @pytest.fixture(name="cdn_prereq_data")
-def get_cdn_prereq_data() -> object:
-    """PyTest Fixture to store prereq data for cdns endpoint.
-    :returns cdn_data: Returns prerequisite data for cdns endpoint
-    """
-    # Response keys for cdns endpoint
-    with open(os.path.join(os.path.dirname(__file__), "prerequisite_data.json"),
-              encoding="utf-8", mode="r") as prereq_file:
-        data = json.load(prereq_file)
-    cdn_data = data["cdns"]
-    return cdn_data
-
-
-def test_cdn_contract(to_session: TOSession, cdn_prereq_data: object,
-                      cdn_post_data: list[dict[str, str] | requests.Response]) -> None:
-    """Test step to validate keys, values and data types from cdns endpoint response.
-    :param to_session: Fixture to get Traffic ops session 
-    :param get_cdn_data: Fixture to get cdn data from a prereq file
-    :param cdn_prereq: Fixture to get sample cdn data and actual cdn response
-    """
-    # validate CDN keys from cdns get response
-    logger.info("Accessing Cdn endpoint through Traffic ops session.")
-    cdn_name = cdn_post_data[0]["name"]
-    cdn_get_response = to_session.get_cdns(
-        query_params={"name": cdn_name})
-    try:
-        cdn_data = cdn_get_response[0]
-        cdn_keys = list(cdn_data[0].keys())
-        logger.info(
-            "CDN Keys from cdns endpoint response %s", cdn_keys)
-        # validate cdn values from prereq data in cdns get response.
-        prereq_values = [cdn_post_data[0]["name"], cdn_post_data[0]
-                         ["domainName"], cdn_post_data[0]["dnssecEnabled"]]
-        get_values = [cdn_data[0]["name"], cdn_data[0]
-                      ["domainName"], cdn_data[0]["dnssecEnabled"]]
-        # validate data types for values from cdn get json response.
-        for (prereq_value, get_value) in zip(prereq_values, get_values):
-            assert isinstance(prereq_value, type(get_value))
-        assert cdn_keys.sort() == list(cdn_prereq_data.keys()).sort()
-        assert get_values == prereq_values
-    except IndexError:
-        logger.error("No CDN data from cdns get request")
-        pytest.fail("Response from get request is empty, Failing test_get_cdn")
-    finally:
-        # Delete CDN after test execution to avoid redundancy.
-        try:
-            cdn_response = cdn_post_data[1]
-            cdn_id = cdn_response["id"]
-            to_session.delete_cdn_by_id(cdn_id=cdn_id)
-        except IndexError:
-            logger.error("CDN wasn't created")
-            pytest.fail(
-                "Response from delete request is empty, Failing test_get_cdn")
+def get_cdn_prereq_data(
+	pytestconfig: pytest.Config
+) -> list[dict[str, object] | list[object] | primitive]:
+	"""
+	PyTest Fixture to store POST request body data for cdns endpoint.
+
+	:returns: Prerequisite data for cdns endpoint.
+	"""
+	prereq_path = pytestconfig.getoption("prerequisites")
+	if not isinstance(prereq_path, str):
+		# unlike the configuration file, this must be present
+		raise ValueError("prereqisites path not configured")
+
+	# Response keys for cdns endpoint
+	data: dict[
+		str,
+		list[dict[str, object] | list[object] | primitive] |\
+			dict[object, object] |\
+			primitive
+		] |\
+	primitive = None
+	with open(prereq_path, encoding="utf-8", mode="r") as prereq_file:
+		data = json.load(prereq_file)
+	if not isinstance(data, dict):
+		raise TypeError(f"prerequisite data must be an object, not '{type(data)}'")
+
+	cdn_data = data["cdns"]
+	if not isinstance(cdn_data, list):
+		raise TypeError(f"cdns data must be a list, not '{type(cdn_data)}'")
+
+	return cdn_data
+
+
+def test_cdn_contract(
+	to_session: TOSession,
+	cdn_prereq_data: list[dict[str, object] | list[object] | primitive],
+	cdn_post_data: dict[str, object]
+) -> None:
+	"""
+	Test step to validate keys, values and data types from cdns endpoint
+	response.
+	:param to_session: Fixture to get Traffic Ops session.
+	:param get_cdn_data: Fixture to get CDN data from a prerequisites file.
+	:param cdn_prereq: Fixture to get sample CDN data and actual CDN response.
+	"""
+	# validate CDN keys from cdns get response
+	logger.info("Accessing /cdns endpoint through Traffic ops session.")
+
+	cdn = cdn_prereq_data[0]
+	if not isinstance(cdn, dict):
+		raise TypeError("malformed cdn in prerequisite data; not an object")
+
+	cdn_name = cdn.get("name")
+	if not isinstance(cdn_name, str):
+		raise TypeError("malformed cdn in prerequisite data; 'name' not a string")
+
+	cdn_get_response: tuple[
+		dict[str, object] | list[dict[str, object] | list[object] | primitive] | primitive,
+		requests.Response
+	] = to_session.get_cdns(query_params={"name": cdn_name})
+	try:
+		cdn_data = cdn_get_response[0]
+		if not isinstance(cdn_data, list):
+			raise TypeError("malformed API response; 'response' property not an array")
+
+		first_cdn = cdn_data[0]
+		if not isinstance(first_cdn, dict):
+			raise TypeError("malformed API response; first CDN in response is not an object")
+		cdn_keys = set(first_cdn.keys())
+
+		logger.info("CDN Keys from cdns endpoint response %s", cdn_keys)
+		# validate cdn values from prereq data in cdns get response.
+		prereq_values = [
+			cdn_post_data["name"],
+			cdn_post_data["domainName"],
+			cdn_post_data["dnssecEnabled"]
+		]
+		get_values = [first_cdn["name"], first_cdn["domainName"], first_cdn["dnssecEnabled"]]
+		# validate data types for values from cdn get json response.
+		for (prereq_value, get_value) in zip(prereq_values, get_values):
+			assert isinstance(prereq_value, type(get_value))
+		assert cdn_keys == set(cdn_post_data.keys())
+		assert get_values == prereq_values
+	except IndexError:
+		logger.error("Either prerequisite data or API response was malformed")
+		pytest.fail("Either prerequisite data or API response was malformed")
+	finally:
+		# Delete CDN after test execution to avoid redundancy.
+		try:
+			cdn_id = cdn_post_data["id"]
+			to_session.delete_cdn_by_id(cdn_id=cdn_id)
+		except IndexError:
+			logger.error("CDN returned by Traffic Ops is missing an 'id' property")
+			pytest.fail("Response from delete request is empty, Failing test_get_cdn")