diff --git a/traffic_ops/testing/api_contract/v4/conftest.py b/traffic_ops/testing/api_contract/v4/conftest.py index 1eebf00f4a..1c9ec5c080 100644 --- a/traffic_ops/testing/api_contract/v4/conftest.py +++ b/traffic_ops/testing/api_contract/v4/conftest.py @@ -26,6 +26,7 @@ from typing import Any, NamedTuple, Union, Optional, TypeAlias from urllib.parse import urlparse import munch +import psycopg2 import pytest import requests @@ -311,6 +312,29 @@ def to_login(to_args: ArgsType) -> TOSession: return to_session +@pytest.fixture(scope="session", name="db_connection") +def open_db_connection(): + """ + Creates new traffic ops db connection. + :returns: New Traffic ops database connection + """ + logger.info(os.getenv("TODB_NAME")) + logger.info(os.getenv("TODB_PASSWORD")) + logger.info(os.getenv("TODB_HOSTNAME")) + logger.info(os.getenv("TODB_USER")) + logger.info(os.getenv("TODB_PORT")) + logger.info(os.getenv("TODB_SSL")) + conn = psycopg2.connect( + user="traffic_ops", + password="twelve", + host="127.0.0.1", + port=5432, + database="traffic_ops", + sslmode="disable" + ) + return conn + + @pytest.fixture(name="request_template_data", scope="session") def request_prerequiste_data(pytestconfig: pytest.Config, request: pytest.FixtureRequest ) -> list[Union[dict[str, object], list[object], Primitive]]: @@ -1153,3 +1177,42 @@ def coordinate_data_post(to_session: TOSession, request_template_data: list[JSON if msg is None: logger.error("coordinate returned by Traffic Ops is missing an 'id' property") pytest.fail("Response from delete request is empty, Failing test_case") + + +@pytest.fixture(name="user_post_data") +def user_data_post(to_session: TOSession, request_template_data: list[JSONData], + tenant_post_data:dict[str, object], db_connection: psycopg2.connect) -> dict[str, object]: + """ + PyTest Fixture to create POST data for users endpoint. + :param to_session: Fixture to get Traffic Ops session. + :param request_template_data: Fixture to get users request template from a prerequisites file. + :returns: Sample POST data and the actual API response. + """ + + user = check_template_data(request_template_data["users"], "users") + + # Return new post data and post response from users POST request + randstr = str(randint(0, 1000)) + try: + username = user["username"] + if not isinstance(username, str): + raise TypeError(f"username must be str, not '{type(username)}'") + user["username"] = username[:4] + randstr + except KeyError as e: + raise TypeError(f"missing user property '{e.args[0]}'") from e + user["tenantId"] = tenant_post_data["id"] + + logger.info("New user data to hit POST method %s", user) + # Hitting users POST methed + response: tuple[JSONData, requests.Response] = to_session.create_user(data=user) + resp_obj = check_template_data(response, "user") + yield resp_obj + coordinate_id = resp_obj.get("id") + # Create a cursor object to interact with the database + cursor = db_connection.cursor() + cursor.execute("DELETE FROM tm_user WHERE id = %s;", (coordinate_id,)) + # Commit the changes + db_connection.commit() + # Close the cursor and the connection + cursor.close() + db_connection.close() diff --git a/traffic_ops/testing/api_contract/v4/data/request_template.json b/traffic_ops/testing/api_contract/v4/data/request_template.json index f1b6dd5d2b..9018202e39 100644 --- a/traffic_ops/testing/api_contract/v4/data/request_template.json +++ b/traffic_ops/testing/api_contract/v4/data/request_template.json @@ -227,13 +227,28 @@ "regex": "/.+", "startTime": "2030-11-09T01:02:03Z", "ttlHours": 72 - } + } ], "coordinates": [ { "name": "test", - "latitude": 38.897663, - "longitude": -77.036574 + "latitude": 38.897663, + "longitude": -77.036574 + } + ], + "users": [ + { + "username": "mike", + "addressLine1": "22 Mike Wazowski You've Got Your Life Back Lane", + "city": "Monstropolis", + "compary": "Monsters Inc.", + "email": "mwazowski@minc.biz", + "fullName": "Mike Wazowski", + "localPasswd": "BFFsully", + "confirmLocalPasswd": "BFFsully", + "newUser": true, + "role": "admin", + "tenantId": 1 } ] } diff --git a/traffic_ops/testing/api_contract/v4/data/response_template.json b/traffic_ops/testing/api_contract/v4/data/response_template.json index c99acfc57b..058faa02e1 100644 --- a/traffic_ops/testing/api_contract/v4/data/response_template.json +++ b/traffic_ops/testing/api_contract/v4/data/response_template.json @@ -1184,5 +1184,144 @@ "type": "string" } } + }, + "users": { + "type": "object", + "required": [ + "addressLine1", + "addressLine2", + "changeLogCount", + "city", + "company", + "country", + "email", + "fullName", + "gid", + "id", + "lastAuthenticated", + "lastUpdated", + "newUser", + "phoneNumber", + "postalCode", + "publicSshKey", + "registrationSent", + "role", + "stateOrProvince", + "tenant", + "tenantId", + "ucdn", + "uid", + "username" + ], + "properties": { + "addressLine1": { + "type": "string" + }, + "addressLine2": { + "type": [ + "string", + "null" + ] + }, + "changeLogCount": { + "type": [ + "integer", + "null" + ] + }, + "city": { + "type": "string" + }, + "company": { + "type": [ + "string", + "null" + ] + }, + "country": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "gid": { + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "integer" + }, + "lastAuthenticated": { + "type": [ + "string", + "null" + ] + }, + "lastUpdated": { + "type": "string" + }, + "newUser": { + "type": "boolean" + }, + "phoneNumber": { + "type": [ + "string", + "null" + ] + }, + "postalCode": { + "type": [ + "string", + "null" + ] + }, + "publicSshKey": { + "type": [ + "string", + "null" + ] + }, + "registrationSent": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "stateOrProvince": { + "type": [ + "string", + "null" + ] + }, + "tenant": { + "type": "string" + }, + "tenantId": { + "type": "integer" + }, + "ucdn": { + "type": "string" + }, + "uid": { + "type": [ + "integer", + "null" + ] + }, + "username": { + "type": "string" + } + } } } diff --git a/traffic_ops/testing/api_contract/v4/requirements.txt b/traffic_ops/testing/api_contract/v4/requirements.txt index 07c255bd51..5076bb3826 100644 --- a/traffic_ops/testing/api_contract/v4/requirements.txt +++ b/traffic_ops/testing/api_contract/v4/requirements.txt @@ -13,4 +13,5 @@ # pytest==7.2.1 -jsonschema==4.17.3 \ No newline at end of file +jsonschema==4.17.3 +psycopg2-binary==2.9.6 \ No newline at end of file diff --git a/traffic_ops/testing/api_contract/v4/test_users.py b/traffic_ops/testing/api_contract/v4/test_users.py new file mode 100644 index 0000000000..e5689785f5 --- /dev/null +++ b/traffic_ops/testing/api_contract/v4/test_users.py @@ -0,0 +1,75 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""API Contract Test Case for users endpoint.""" +import logging +from typing import Union + +import pytest +import requests +from jsonschema import validate + +from trafficops.tosession import TOSession + +# Create and configure logger +logger = logging.getLogger() + +Primitive = Union[bool, int, float, str, None] + +def test_user_contract(to_session: TOSession, + response_template_data: dict[str, Union[Primitive, + list[Union[Primitive, dict[str, object], list[object]]], + dict[object, object]]], user_post_data: dict[str, object]) -> None: + """ + Test step to validate keys, values and data types from users endpoint + response. + :param to_session: Fixture to get Traffic Ops session. + :param response_template_data: Fixture to get response template data from a prerequisites file. + :param user_post_data: Fixture to get sample user data and actual user response. + """ + # validate user keys from users get response + logger.info("Accessing /users endpoint through Traffic ops session.") + + user_id = user_post_data.get("id") + if not isinstance(user_id, int): + raise TypeError("malformed user in prerequisite data; 'id' not a integer") + + user_get_response: tuple[ + Union[dict[str, object], list[Union[dict[str, object], list[object], Primitive]], Primitive], + requests.Response + ] = to_session.get_user_by_id(user_id=user_id) + try: + user_data = user_get_response[0] + if not isinstance(user_data, list): + raise TypeError("malformed API response; 'response' property not an array") + + first_user = user_data[0] + if not isinstance(first_user, dict): + raise TypeError("malformed API response; first user in response is not an dict") + logger.info("user Api get response %s", first_user) + + user_response_template = response_template_data.get("users") + if not isinstance(user_response_template, dict): + raise TypeError( + f"user response template data must be a dict, not '{type(user_response_template)}'") + + # validate user values from prereq data in users get response. + prereq_values = [user_post_data["username"], user_post_data["tenantId"]] + get_values = [first_user["username"], first_user["tenantId"]] + + assert validate(instance=first_user, schema=user_response_template) is None + assert get_values == prereq_values + except IndexError: + logger.error("Either prerequisite data or API response was malformed") + pytest.fail("API contract test failed for cdn endpoint: API response was malformed")