Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: API Tests

on: [push]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: pip install -r requirements.txt

- name: Start Flask app
run: |
python app.py &
sleep 2

- name: Run tests
run: pytest test_pet.py test_store.py -v --html=report.html --self-contained-html

- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: pytest-html-report
path: report.html
24 changes: 23 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,26 @@ test_scratch.py
pet_scratch.py
store_scratch.py
report.html
style.css
style.css
.idea/

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/

# Pytest
.pytest_cache/
.coverage
htmlcov/

# Ignore html report
*.html
reports/
playwright-report/
allure-results/
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/copilot.data.migration.agent.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions .idea/pytest-api-example.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 0 additions & 50 deletions README.md

This file was deleted.

69 changes: 69 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Petstore API

A simple REST API built with Flask and flask-restx, with a pytest test suite and GitHub Actions CI.

## Project Structure

```
pytest-api-example/
├── .github/
│ └── workflows/
│ └── pytest.yml # GitHub Actions CI workflow
├── app.py # Flask API
├── api_helpers.py # HTTP helper functions for tests
├── schemas.py # JSON schemas for response validation
├── test_pet.py # Tests for /pets endpoints
├── test_store.py # Tests for /store endpoints
└── requirements.txt # Project dependencies
```

## Setup

```bash
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # Mac/Linux
venv\Scripts\activate # Windows

# Install dependencies
pip install -r requirements.txt
```

## Running the API

```bash
python app.py
```

API will be available at `http://127.0.0.1:5000`
Swagger docs at `http://127.0.0.1:5000`

## Running Tests

```bash
# Run all tests
pytest test_pet.py test_store.py -v

# Run pet tests only
pytest test_pet.py -v

# Run store tests only
pytest test_store.py -v
```

## CI/CD

GitHub Actions workflow runs automatically on every push.
Results are visible under the **Actions** tab in the repository.

## Known Bugs

| # | Location | Description |
|---|---|---|
| 1 | `schemas.py` line 9 | `name` typed as `integer` instead of `string` |
| 2 | `app.py` line 101 | Missing f-string in `findByStatus` abort message |
| 3 | `app.py` | `POST /pets` missing `validate=True` — accepts invalid enum values |
| 4 | `app.py` | `POST /pets` allows client-supplied IDs |
| 5 | `app.py` | No `DELETE` endpoints — fixed by adding DELETE to `app.py` and `api_helpers.py` |
| 6 | `app.py` | `PATCH /order` mutates state before validating status |
| 7 | `app.py` | `findByStatus` returns misleading error when status param is missing |
7 changes: 6 additions & 1 deletion api_helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import requests

base_url = 'http://localhost:5000'
base_url = 'http://127.0.0.1:5000'

# GET requests
def get_api_data(endpoint, params = {}):
Expand All @@ -15,4 +15,9 @@ def post_api_data(endpoint, data):
# PATCH requests
def patch_api_data(endpoint, data):
response = requests.patch(f'{base_url}{endpoint}', json=data)
return response

# DELETE requests
def delete_api_data(endpoint):
response = requests.delete(f'{base_url}{endpoint}')
return response
15 changes: 12 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def get(self):
"""Find pets by status"""
status = request.args.get('status')
if status not in PET_STATUS:
api.abort(400, 'Invalid pet status {status}')
api.abort(400, f'Invalid pet status {status}')
if status:
filtered_pets = [pet for pet in pets if pet['status'] == status]
return filtered_pets
Expand Down Expand Up @@ -161,10 +161,19 @@ def patch(self, order_id):
elif update_data['status'] == 'available':
pet['status'] = 'available'
else:
api.abort(400, f"Invalid status '{update_data['status']}'. Valid statuses are {', '.join(PET_STATUS)}")

api.abort(400, f"Invalid status '{update_data['status']}'. Valid statuses are {', '.join(PET_STATUS)}")\

return {"message": "Order and pet status updated successfully"}

# ADDED: DELETE /store/order/<order_id>
@store_ns.doc('delete_order')
@store_ns.response(200, 'Order deleted')
def delete(self, order_id):
"""Delete an order by id"""
if order_id not in orders:
api.abort(404, "Order not found")
del orders[order_id]
return {"message": f"Order {order_id} deleted successfully"}

if __name__ == '__main__':
app.run(debug=True)
36 changes: 36 additions & 0 deletions notes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
1) Bug #1 line9/schemas.py : Bug in schema declaration "name": {"type": "integer"} -> Should be "string" datatype
It caused ValidationError is case or running GET call for test "test_pet_schema()""

2) Bug #2: line101/app.py : missing f-sting for placeholder.
The error message would literally print {status} instead of the actual value


3) Bug #3: Missing enum validation in POST call /pets/ The API accepts pets with invalid enum values like:
type': 'bird', 'status': 'reserved' in payload.
No validate=True on @expect, so flask-restx doesn't enforce type or status enum values.
That why 2 last tests will fail:
test_pet.py::test_create_pet_invalid_enum_400[pet_data0]
test_pet.py::test_create_pet_invalid_enum_400[pet_data1]
Marked by xfail marker
POST /pets accepts invalid enum values


4) BUG #4 POST /pets allows client supplied IDs
id is not readonly=True in pet_model, so clients can pass any ID including duplicates or negatives.
IDs should be server-generated.

5) BUG #5 Missing DELETE endpoints
DELETE /pets/<id> or DELETE /store/order/<id>.
Broke fixture teardown pets stayed pending, exhausting available pool and causing tests to skip.
Fixed by adding DELETE endpoints to app.py and extending api_helpers.py with delete_api_data()

6) Bug #6 PATCH /order mutates state before validating
order['status'] and pet['status'] are both updated before the else: abort(400) runs.
Invalid status partially corrupts state before the error is raised. Confirmed by test_patch_order_by_id_400 xfail.

7) BUG #7 findByStatus wrong null check
If we call GET /pets/findByStatus without a status parameter,
instead of a helpful error like "status parameter is required",
the API returns "Invalid pet status None" which is confusing and misleading.

8) I'd all tests to test Class to run all together or added specific pytest test marker
8 changes: 8 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Flask==3.1.2
flask-restx==1.3.2
pytest==9.0.2
pytest-html
allure-pytest
requests==2.32.5
jsonschema==4.26.0
PyHamcrest==2.1.0
Loading