Skip to content

Commit 202ec19

Browse files
committed
Added Formatter And Allure reporting
1 parent 6b1d805 commit 202ec19

File tree

10 files changed

+277
-146
lines changed

10 files changed

+277
-146
lines changed

conftest.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Global Fixtures"""
2+
23
import pytest
3-
from helpers.api_client import APIClient
44

5+
from helpers.api_client import APIClient
56

67
BASE_URI = "http://localhost:3000"
78
BASE_PATH = "/api/books"
@@ -10,21 +11,23 @@
1011
@pytest.fixture(scope="module")
1112
def api_client():
1213
"""API Client Global Fixture"""
13-
return APIClient(BASE_URI, BASE_PATH)
14+
return APIClient(BASE_URI, BASE_PATH)
1415

1516

1617
def pytest_sessionfinish(session, exitstatus):
1718
"""
1819
Called after whole test run is complete, after all parallel workers.
1920
Runs in the master process.
2021
"""
21-
if hasattr(session.config, 'workerinput'):
22+
if hasattr(session.config, "workerinput"):
2223
# This is a worker node (used by xdist), skip cleanup
2324
return
2425

2526
client = APIClient(BASE_URI, BASE_PATH)
26-
res = client.delete(client.build_url("/reset"),
27-
headers={"authorization": "Bearer admin-token"})
28-
27+
res = client.delete(
28+
client.build_url("/reset"), headers={"authorization": "Bearer admin-token"}
29+
)
2930

30-
assert res.status_code == 204, f"Reset failed: {res.status_code} {res.text}" # nosec
31+
assert (
32+
res.status_code == 204
33+
), f"Reset failed: {res.status_code} {res.text}" # nosec

helpers/api_client.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""API Client Utility"""
2+
23
import logging
34
from urllib.parse import urljoin
4-
from urllib3.util.retry import Retry
5+
56
import requests
67
from requests.adapters import HTTPAdapter
8+
from urllib3.util.retry import Retry
79

810
logger = logging.getLogger(__name__)
911

@@ -31,7 +33,7 @@ def increment(self, *args, **kwargs):
3133
last.status,
3234
last.error,
3335
len(new_retry.history),
34-
self.total
36+
self.total,
3537
)
3638
return new_retry
3739

@@ -44,12 +46,19 @@ def __init__(self, base_url: str, base_path: str = "", headers: dict = None):
4446
self.base_url = base_url.rstrip("/") + "/"
4547
self.base_path = base_path.rstrip("/") + "/" if base_path else ""
4648
self.headers.update(headers or {})
47-
self.hooks['response'].append(self.log_response)
49+
self.hooks["response"].append(self.log_response)
4850
retries = LoggingRetry(
4951
total=3,
5052
status_forcelist=[429, 500, 502, 503, 504],
51-
allowed_methods=["HEAD", "GET", "OPTIONS",
52-
"POST", "PUT", "DELETE", "PATCH"]
53+
allowed_methods=[
54+
"HEAD",
55+
"GET",
56+
"OPTIONS",
57+
"POST",
58+
"PUT",
59+
"DELETE",
60+
"PATCH",
61+
],
5362
)
5463
adapter = HTTPAdapter(max_retries=retries)
5564
self.mount("https://", adapter)
@@ -58,15 +67,19 @@ def __init__(self, base_url: str, base_path: str = "", headers: dict = None):
5867
def build_url(self, endpoint: str = None) -> str:
5968
"""Build a full URL for the given endpoint."""
6069
endpoint = endpoint.lstrip("/") if endpoint else ""
61-
fullpath = f'{self.base_path}{endpoint}'
70+
fullpath = f"{self.base_path}{endpoint}"
6271
return urljoin(self.base_url, fullpath)
6372

6473
def log_response(self, response: requests.Response, *args, **kwargs):
6574
"""Log details of the request and response."""
6675
logger.debug("Request Headers: %s", response.request.headers)
6776
logger.debug("Request Body: %s", response.request.body)
68-
logger.info("%s : %s -> %s", response.request.method,
69-
response.request.url, response.status_code)
77+
logger.info(
78+
"%s : %s -> %s",
79+
response.request.method,
80+
response.request.url,
81+
response.status_code,
82+
)
7083
logger.debug("Response Headers: %s", response.headers)
7184
logger.debug("Response Body: %s", response.text)
7285
return response

helpers/validator.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Validator and Assertion Utility"""
2+
23
import logging
4+
35
import requests
46

57
logger = logging.getLogger(__name__)
@@ -33,22 +35,23 @@ def validate_status_code(response: requests.Response, expected_status_code: int)
3335
"""Assert that response.status_code == expected_status_code."""
3436
_assert_with_log(
3537
response.status_code == expected_status_code,
36-
f"StatusCode => Expected: {expected_status_code} Actual: {response.status_code}"
38+
f"StatusCode => Expected: {expected_status_code} Actual: {response.status_code}",
3739
)
3840

3941

4042
def validate_response_book(response: requests.Response, expected_book: dict):
4143
"""Validate a single book response matches the expected book."""
4244
res_json = response.json()
43-
_assert_with_log(res_json.get("id") is not None,
44-
"Book ID should be auto generated")
45-
_assert_with_log(res_json.get('author') ==
46-
expected_book.get('author'), "Book Author Name")
47-
_assert_with_log(res_json.get('title') ==
48-
expected_book.get('title'), "Book Title")
45+
_assert_with_log(res_json.get("id") is not None, "Book ID should be auto generated")
46+
_assert_with_log(
47+
res_json.get("author") == expected_book.get("author"), "Book Author Name"
48+
)
49+
_assert_with_log(res_json.get("title") == expected_book.get("title"), "Book Title")
4950

5051

5152
def validate_error_message(response: requests.Response, message: str):
5253
"""Validate the error message in the response."""
53-
_assert_with_log(response.json().get('error') == message,
54-
f"Error Message: Expected '{message}' Actual '{response.json().get('error')}'")
54+
_assert_with_log(
55+
response.json().get("error") == message,
56+
f"Error Message: Expected '{message}' Actual '{response.json().get('error')}'",
57+
)

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ filterwarnings =
55
markers =
66
description: Adds a description to the test for documentation or reporting
77

8-
addopts = -v -s --tb=short --html=test-results/report.html --self-contained-html
8+
addopts = -v -s --html=test-results/report.html --self-contained-html --alluredir=test-results/allure-results
99
testpaths = tests
1010
python_files = test_*.py *_test.py
1111

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pytest-dependency
44
pytest-xdist
55
pytest-html
66
pylint
7-
bandit
7+
bandit
8+
allure-pytest

tests/BaseTest.py renamed to tests/base_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Base Test Module"""
2+
23
import pytest
4+
35
from helpers.api_client import APIClient
46

57

68
class BaseTest:
79
"""Base Test Class"""
10+
811
client: APIClient
912

1013
@pytest.fixture(scope="class", autouse=True)

tests/test_create_book.py

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,107 @@
11
"""Create Book Test Module"""
2+
3+
import allure
24
import pytest
3-
from tests.BaseTest import BaseTest
5+
46
from helpers import validator
7+
from tests.base_test import BaseTest
58

69

10+
@allure.epic("Book Management")
11+
@allure.feature("Create Book")
12+
@allure.severity(allure.severity_level.CRITICAL)
713
class TestCreateBook(BaseTest):
8-
"""Creat Book Test Class"""
14+
"""Create Book Test Class"""
15+
916
HEADERS = {"authorization": "Bearer user-token"}
1017

11-
def create_and_validate_book(self, book, expected_status=201, expected_message=None):
18+
def create_and_validate_book(
19+
self, book, expected_status=201, expected_message=None
20+
):
1221
"""Helper to create a book and validate the response."""
1322
response = self.client.post(
14-
self.client.build_url(), json=book, headers=self.HEADERS)
23+
self.client.build_url(), json=book, headers=self.HEADERS
24+
)
1525
validator.validate_status_code(response, expected_status)
1626
if expected_status == 201:
1727
validator.validate_response_book(response, book)
1828
elif expected_message:
1929
validator.validate_error_message(response, expected_message)
2030
return response
2131

32+
@allure.title("Should create book when title and author are valid")
2233
def test_should_create_book_when_title_and_author_are_valid(self):
2334
"""Test creating a book with valid title and author."""
24-
book = {"title": "New Book POST API Title",
25-
"author": "New Book POST API Author"}
35+
book = {
36+
"title": "New Book POST API Title",
37+
"author": "New Book POST API Author",
38+
}
2639
self.create_and_validate_book(book)
2740

41+
@allure.title("Should reject duplicate book creation")
2842
def test_should_reject_duplicate_book_creation(self):
2943
"""Test duplicate book creation returns 409."""
30-
book = {"title": "New Book POST API Title",
31-
"author": "New Book POST API Author"}
44+
book = {
45+
"title": "New Book POST API Title",
46+
"author": "New Book POST API Author",
47+
}
3248
self.create_and_validate_book(
33-
book, expected_status=409, expected_message="A book with the same title and author already exists")
49+
book,
50+
expected_status=409,
51+
expected_message="A book with the same title and author already exists",
52+
)
3453

54+
@allure.title("Should create book when title is different for same author")
3555
def test_should_create_book_when_title_is_different_for_same_author(self):
3656
"""Test creating a book with a different title for the same author."""
37-
book = {"title": "New Book POST API Title Different",
38-
"author": "New Book POST API Author"}
57+
book = {
58+
"title": "New Book POST API Title Different",
59+
"author": "New Book POST API Author",
60+
}
3961
self.create_and_validate_book(book)
4062

63+
@allure.title("Should create book when author is different for same book")
4164
def test_should_create_book_when_author_is_different_for_same_book(self):
4265
"""Test creating a book with a different author for the same title."""
43-
book = {"title": "New Book POST API Title Different",
44-
"author": "New Book POST API Author Different"}
66+
book = {
67+
"title": "New Book POST API Title Different",
68+
"author": "New Book POST API Author Different",
69+
}
4570
self.create_and_validate_book(book)
4671

72+
@allure.title("Should return 401 when no auth token provided")
4773
def test_should_return_401_when_no_auth_token_provided(self):
4874
"""Test creating a book without authentication returns 401."""
49-
book = {"title": "Error 401 Test Title",
50-
"author": "Error 401 Test Author"}
75+
book = {"title": "Error 401 Test Title", "author": "Error 401 Test Author"}
5176
response = self.client.post(self.client.build_url(), json=book)
5277
validator.validate_status_code(response, 401)
53-
validator.validate_error_message(
54-
response, "Unauthorized. No token provided.")
78+
validator.validate_error_message(response, "Unauthorized. No token provided.")
5579

56-
@pytest.mark.parametrize("payload,expected_message", [
57-
({}, "Both title and author are required."),
58-
({"author": "author only test"}, "Both title and author are required."),
59-
({"title": "title only test"}, "Both title and author are required."),
60-
])
80+
@pytest.mark.parametrize(
81+
"payload,expected_message",
82+
[
83+
({}, "Both title and author are required."),
84+
({"author": "author only test"}, "Both title and author are required."),
85+
({"title": "title only test"}, "Both title and author are required."),
86+
],
87+
)
88+
@allure.title("Should reject book with missing fields {expected_message}")
6189
def test_should_reject_book_with_missing_fields(self, payload, expected_message):
6290
"""Test creating a book with missing fields returns 400."""
6391
self.create_and_validate_book(
64-
payload, expected_status=400, expected_message=expected_message)
92+
payload, expected_status=400, expected_message=expected_message
93+
)
6594

95+
@allure.title("Should reject book creation with client-provided ID")
6696
def test_should_reject_book_creation_with_client_provided_id(self):
6797
"""Test creating a book with a client-provided ID returns 400."""
68-
book = {"id": "122233", "title": "Client provided Book ID Title",
69-
"author": "Client provided Book Author"}
98+
book = {
99+
"id": "122233",
100+
"title": "Client provided Book ID Title",
101+
"author": "Client provided Book Author",
102+
}
70103
self.create_and_validate_book(
71-
book, expected_status=400, expected_message="ID must not be provided when creating a book")
104+
book,
105+
expected_status=400,
106+
expected_message="ID must not be provided when creating a book",
107+
)

tests/test_delete_book.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Delete Book Test Module"""
2+
import allure
23
import pytest
34
from helpers import validator
4-
from tests.BaseTest import BaseTest
5-
5+
from tests.base_test import BaseTest
66

7+
@allure.epic("Book Management")
8+
@allure.feature("Delete Book")
9+
@allure.severity(allure.severity_level.MINOR)
710
class TestDeleteBook(BaseTest):
811
"""Delete Book Test class"""
912

@@ -14,41 +17,48 @@ class TestDeleteBook(BaseTest):
1417
@pytest.fixture(scope="class", autouse=True)
1518
def test_create_book_before_delete_book_test(self, init_api_client):
1619
"""Create a book to be used for delete tests."""
17-
book = {"title": "Delete API Test Book Title",
18-
"author": "Delete API Test Book Author"}
20+
book = {
21+
"title": "Delete API Test Book Title",
22+
"author": "Delete API Test Book Author",
23+
}
1924
response = self.client.post(
20-
self.client.build_url(), json=book, headers=self.ADMIN_HEADERS)
25+
self.client.build_url(), json=book, headers=self.ADMIN_HEADERS
26+
)
2127
validator.validate_status_code(response, 201)
2228
validator.validate_response_book(response, book)
23-
self.__class__.book_id = response.json()['id']
29+
self.__class__.book_id = response.json()["id"]
2430

31+
@allure.title("Should return 401 when no auth token provided on delete")
2532
def test_should_return_401_when_no_auth_token_provided_on_delete(self):
2633
"""Test deleting a book without authentication returns 401."""
27-
response = self.client.delete(
28-
self.client.build_url(f"/{self.book_id}"))
34+
response = self.client.delete(self.client.build_url(f"/{self.book_id}"))
2935
validator.validate_status_code(response, 401)
30-
validator.validate_error_message(
31-
response, "Unauthorized. No token provided.")
36+
validator.validate_error_message(response, "Unauthorized. No token provided.")
3237

38+
@allure.title("Should return 403 when user auth token is provided on delete")
3339
def test_should_return_403_when_user_auth_token_is_provided_on_delete(self):
3440
"""Test deleting a book with a user token returns 403."""
35-
response = self.client.delete(self.client.build_url(
36-
f"/{self.book_id}"), headers=self.USER_HEADERS)
41+
response = self.client.delete(
42+
self.client.build_url(f"/{self.book_id}"), headers=self.USER_HEADERS
43+
)
3744
validator.validate_status_code(response, 403)
38-
validator.validate_error_message(
39-
response, "Forbidden. Admin access required.")
45+
validator.validate_error_message(response, "Forbidden. Admin access required.")
4046

4147
@pytest.mark.dependency(name="delete_valid_book")
48+
@allure.title("Should delete book when book ID is valid")
4249
def test_should_delete_book_when_book_id_is_valid(self):
4350
"""Test deleting a book with admin token returns 204."""
44-
response = self.client.delete(self.client.build_url(
45-
f"/{self.book_id}"), headers=self.ADMIN_HEADERS)
51+
response = self.client.delete(
52+
self.client.build_url(f"/{self.book_id}"), headers=self.ADMIN_HEADERS
53+
)
4654
validator.validate_status_code(response, 204)
4755

4856
@pytest.mark.dependency(depends=["delete_valid_book"])
57+
@allure.title("Should return 404 when book is already deleted or does not exist")
4958
def test_should_return_404_when_book_is_already_deleted_or_not_exists(self):
5059
"""Test deleting a book that is already deleted or does not exist returns 404."""
51-
response = self.client.delete(self.client.build_url(
52-
f"/{self.book_id}"), headers=self.ADMIN_HEADERS)
60+
response = self.client.delete(
61+
self.client.build_url(f"/{self.book_id}"), headers=self.ADMIN_HEADERS
62+
)
5363
validator.validate_status_code(response, 404)
5464
validator.validate_error_message(response, "Book not found")

0 commit comments

Comments
 (0)