Spaces:
Paused
Paused
Upload 7 files
Browse files- tests/api/test_auth.py +74 -0
- tests/api/test_branches.py +154 -0
- tests/api/test_orders.py +143 -0
- tests/api/test_products.py +128 -0
- tests/conftest.py +84 -0
- tests/test.env +26 -0
- tests/utils.py +104 -0
tests/api/test_auth.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from httpx import AsyncClient
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
+
from app.core.security import create_access_token
|
| 5 |
+
|
| 6 |
+
pytestmark = pytest.mark.asyncio
|
| 7 |
+
|
| 8 |
+
async def test_login(client: AsyncClient, db_session: AsyncSession):
|
| 9 |
+
# First register a test user
|
| 10 |
+
register_response = await client.post(
|
| 11 |
+
"/api/v1/auth/register",
|
| 12 |
+
data={
|
| 13 |
+
"username": "testuser@example.com",
|
| 14 |
+
"password": "testpass123"
|
| 15 |
+
}
|
| 16 |
+
)
|
| 17 |
+
assert register_response.status_code == 200
|
| 18 |
+
|
| 19 |
+
# Test login with correct credentials
|
| 20 |
+
response = await client.post(
|
| 21 |
+
"/api/v1/auth/login",
|
| 22 |
+
data={
|
| 23 |
+
"username": "testuser@example.com",
|
| 24 |
+
"password": "testpass123"
|
| 25 |
+
}
|
| 26 |
+
)
|
| 27 |
+
assert response.status_code == 200
|
| 28 |
+
assert "access_token" in response.json()
|
| 29 |
+
assert response.json()["token_type"] == "bearer"
|
| 30 |
+
|
| 31 |
+
# Test login with incorrect password
|
| 32 |
+
response = await client.post(
|
| 33 |
+
"/api/v1/auth/login",
|
| 34 |
+
data={
|
| 35 |
+
"username": "testuser@example.com",
|
| 36 |
+
"password": "wrongpass"
|
| 37 |
+
}
|
| 38 |
+
)
|
| 39 |
+
assert response.status_code == 401
|
| 40 |
+
|
| 41 |
+
# Test login with non-existent user
|
| 42 |
+
response = await client.post(
|
| 43 |
+
"/api/v1/auth/login",
|
| 44 |
+
data={
|
| 45 |
+
"username": "nonexistent@example.com",
|
| 46 |
+
"password": "testpass123"
|
| 47 |
+
}
|
| 48 |
+
)
|
| 49 |
+
assert response.status_code == 401
|
| 50 |
+
|
| 51 |
+
async def test_register(client: AsyncClient, db_session: AsyncSession):
|
| 52 |
+
# Test successful registration
|
| 53 |
+
response = await client.post(
|
| 54 |
+
"/api/v1/auth/register",
|
| 55 |
+
data={
|
| 56 |
+
"username": "newuser@example.com",
|
| 57 |
+
"password": "newpass123"
|
| 58 |
+
}
|
| 59 |
+
)
|
| 60 |
+
assert response.status_code == 200
|
| 61 |
+
user_data = response.json()
|
| 62 |
+
assert user_data["email"] == "newuser@example.com"
|
| 63 |
+
assert "hashed_password" not in user_data
|
| 64 |
+
|
| 65 |
+
# Test registration with existing email
|
| 66 |
+
response = await client.post(
|
| 67 |
+
"/api/v1/auth/register",
|
| 68 |
+
data={
|
| 69 |
+
"username": "newuser@example.com",
|
| 70 |
+
"password": "anotherpass"
|
| 71 |
+
}
|
| 72 |
+
)
|
| 73 |
+
assert response.status_code == 400
|
| 74 |
+
assert "Email already registered" in response.json()["detail"]
|
tests/api/test_branches.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from httpx import AsyncClient
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
+
from ..utils import create_user_and_login, create_branch
|
| 5 |
+
|
| 6 |
+
pytestmark = pytest.mark.asyncio
|
| 7 |
+
|
| 8 |
+
@pytest.fixture
|
| 9 |
+
async def admin_token(client: AsyncClient) -> str:
|
| 10 |
+
"""Create an admin user and return their token"""
|
| 11 |
+
user_data = await create_user_and_login(
|
| 12 |
+
client,
|
| 13 |
+
email="admin@example.com",
|
| 14 |
+
password="adminpass123",
|
| 15 |
+
full_name="Admin User",
|
| 16 |
+
is_superuser=True
|
| 17 |
+
)
|
| 18 |
+
return user_data["token"]
|
| 19 |
+
|
| 20 |
+
@pytest.fixture
|
| 21 |
+
async def regular_token(client: AsyncClient) -> str:
|
| 22 |
+
"""Create a regular user and return their token"""
|
| 23 |
+
user_data = await create_user_and_login(
|
| 24 |
+
client,
|
| 25 |
+
email="user@example.com",
|
| 26 |
+
password="userpass123",
|
| 27 |
+
full_name="Regular User",
|
| 28 |
+
is_superuser=False
|
| 29 |
+
)
|
| 30 |
+
return user_data["token"]
|
| 31 |
+
|
| 32 |
+
async def test_create_branch(client: AsyncClient, admin_token: str):
|
| 33 |
+
# Test creating a branch as admin
|
| 34 |
+
response = await client.post(
|
| 35 |
+
"/api/v1/branches/",
|
| 36 |
+
json={
|
| 37 |
+
"name": "Test Branch",
|
| 38 |
+
"location": "Test Location",
|
| 39 |
+
"contact_email": "branch@example.com",
|
| 40 |
+
"phone": "1234567890"
|
| 41 |
+
},
|
| 42 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 43 |
+
)
|
| 44 |
+
assert response.status_code == 200
|
| 45 |
+
data = response.json()
|
| 46 |
+
assert data["name"] == "Test Branch"
|
| 47 |
+
assert data["location"] == "Test Location"
|
| 48 |
+
return data["id"]
|
| 49 |
+
|
| 50 |
+
async def test_create_branch_unauthorized(client: AsyncClient, regular_token: str):
|
| 51 |
+
# Test creating a branch as regular user (should fail)
|
| 52 |
+
response = await client.post(
|
| 53 |
+
"/api/v1/branches/",
|
| 54 |
+
json={
|
| 55 |
+
"name": "Test Branch",
|
| 56 |
+
"location": "Test Location"
|
| 57 |
+
},
|
| 58 |
+
headers={"Authorization": f"Bearer {regular_token}"}
|
| 59 |
+
)
|
| 60 |
+
assert response.status_code == 403
|
| 61 |
+
|
| 62 |
+
async def test_get_branch(client: AsyncClient, admin_token: str):
|
| 63 |
+
# First create a branch
|
| 64 |
+
branch_id = await test_create_branch(client, admin_token)
|
| 65 |
+
|
| 66 |
+
# Test getting the branch
|
| 67 |
+
response = await client.get(
|
| 68 |
+
f"/api/v1/branches/{branch_id}",
|
| 69 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 70 |
+
)
|
| 71 |
+
assert response.status_code == 200
|
| 72 |
+
data = response.json()
|
| 73 |
+
assert data["id"] == branch_id
|
| 74 |
+
assert data["name"] == "Test Branch"
|
| 75 |
+
|
| 76 |
+
async def test_list_branches(client: AsyncClient, admin_token: str):
|
| 77 |
+
# Create multiple branches
|
| 78 |
+
await test_create_branch(client, admin_token)
|
| 79 |
+
await client.post(
|
| 80 |
+
"/api/v1/branches/",
|
| 81 |
+
json={
|
| 82 |
+
"name": "Another Branch",
|
| 83 |
+
"location": "Another Location",
|
| 84 |
+
"contact_email": "another@example.com",
|
| 85 |
+
"phone": "0987654321"
|
| 86 |
+
},
|
| 87 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Test listing branches
|
| 91 |
+
response = await client.get(
|
| 92 |
+
"/api/v1/branches/",
|
| 93 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 94 |
+
)
|
| 95 |
+
assert response.status_code == 200
|
| 96 |
+
data = response.json()
|
| 97 |
+
assert len(data) >= 2
|
| 98 |
+
assert any(b["name"] == "Test Branch" for b in data)
|
| 99 |
+
assert any(b["name"] == "Another Branch" for b in data)
|
| 100 |
+
|
| 101 |
+
async def test_update_branch(client: AsyncClient, admin_token: str):
|
| 102 |
+
# First create a branch
|
| 103 |
+
branch_id = await test_create_branch(client, admin_token)
|
| 104 |
+
|
| 105 |
+
# Test updating the branch
|
| 106 |
+
response = await client.put(
|
| 107 |
+
f"/api/v1/branches/{branch_id}",
|
| 108 |
+
json={
|
| 109 |
+
"name": "Updated Branch",
|
| 110 |
+
"location": "Updated Location",
|
| 111 |
+
"contact_email": "updated@example.com",
|
| 112 |
+
"phone": "1112223333"
|
| 113 |
+
},
|
| 114 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 115 |
+
)
|
| 116 |
+
assert response.status_code == 200
|
| 117 |
+
data = response.json()
|
| 118 |
+
assert data["name"] == "Updated Branch"
|
| 119 |
+
assert data["location"] == "Updated Location"
|
| 120 |
+
assert data["contact_email"] == "updated@example.com"
|
| 121 |
+
|
| 122 |
+
async def test_delete_branch(client: AsyncClient, admin_token: str):
|
| 123 |
+
# First create a branch
|
| 124 |
+
branch_id = await test_create_branch(client, admin_token)
|
| 125 |
+
|
| 126 |
+
# Test deleting the branch
|
| 127 |
+
response = await client.delete(
|
| 128 |
+
f"/api/v1/branches/{branch_id}",
|
| 129 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 130 |
+
)
|
| 131 |
+
assert response.status_code == 200
|
| 132 |
+
|
| 133 |
+
# Verify branch is deleted
|
| 134 |
+
response = await client.get(
|
| 135 |
+
f"/api/v1/branches/{branch_id}",
|
| 136 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 137 |
+
)
|
| 138 |
+
assert response.status_code == 404
|
| 139 |
+
|
| 140 |
+
async def test_branch_statistics(client: AsyncClient, admin_token: str):
|
| 141 |
+
# First create a branch
|
| 142 |
+
branch_id = await test_create_branch(client, admin_token)
|
| 143 |
+
|
| 144 |
+
# Test getting branch statistics
|
| 145 |
+
response = await client.get(
|
| 146 |
+
f"/api/v1/branches/{branch_id}/statistics",
|
| 147 |
+
headers={"Authorization": f"Bearer {admin_token}"}
|
| 148 |
+
)
|
| 149 |
+
assert response.status_code == 200
|
| 150 |
+
data = response.json()
|
| 151 |
+
assert "total_products" in data
|
| 152 |
+
assert "total_orders" in data
|
| 153 |
+
assert "total_revenue" in data
|
| 154 |
+
assert "average_order_value" in data
|
tests/api/test_orders.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from httpx import AsyncClient
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
+
|
| 5 |
+
pytestmark = pytest.mark.asyncio
|
| 6 |
+
|
| 7 |
+
@pytest.fixture
|
| 8 |
+
async def user_token(client: AsyncClient) -> str:
|
| 9 |
+
"""Create a test user and return their token"""
|
| 10 |
+
# Register user
|
| 11 |
+
await client.post(
|
| 12 |
+
"/api/v1/auth/register",
|
| 13 |
+
data={
|
| 14 |
+
"username": "testuser@example.com",
|
| 15 |
+
"password": "testpass123",
|
| 16 |
+
"full_name": "Test User"
|
| 17 |
+
}
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Login to get token
|
| 21 |
+
response = await client.post(
|
| 22 |
+
"/api/v1/auth/login",
|
| 23 |
+
data={
|
| 24 |
+
"username": "testuser@example.com",
|
| 25 |
+
"password": "testpass123"
|
| 26 |
+
}
|
| 27 |
+
)
|
| 28 |
+
return response.json()["access_token"]
|
| 29 |
+
|
| 30 |
+
@pytest.fixture
|
| 31 |
+
async def test_product(client: AsyncClient, user_token: str) -> dict:
|
| 32 |
+
"""Create a test product and return its data"""
|
| 33 |
+
response = await client.post(
|
| 34 |
+
"/api/v1/products/",
|
| 35 |
+
json={
|
| 36 |
+
"name": "Test Product",
|
| 37 |
+
"description": "A test product",
|
| 38 |
+
"price": 99.99,
|
| 39 |
+
"category": "test",
|
| 40 |
+
"inventory_count": 10
|
| 41 |
+
},
|
| 42 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 43 |
+
)
|
| 44 |
+
return response.json()
|
| 45 |
+
|
| 46 |
+
async def test_create_order(client: AsyncClient, user_token: str, test_product: dict):
|
| 47 |
+
# Test creating an order
|
| 48 |
+
response = await client.post(
|
| 49 |
+
"/api/v1/orders/",
|
| 50 |
+
json={
|
| 51 |
+
"items": [
|
| 52 |
+
{
|
| 53 |
+
"product_id": test_product["id"],
|
| 54 |
+
"quantity": 2
|
| 55 |
+
}
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 59 |
+
)
|
| 60 |
+
assert response.status_code == 200
|
| 61 |
+
data = response.json()
|
| 62 |
+
assert len(data["items"]) == 1
|
| 63 |
+
assert data["items"][0]["product_id"] == test_product["id"]
|
| 64 |
+
assert data["total_amount"] == test_product["price"] * 2
|
| 65 |
+
return data["id"]
|
| 66 |
+
|
| 67 |
+
async def test_get_order(client: AsyncClient, user_token: str, test_product: dict):
|
| 68 |
+
# First create an order
|
| 69 |
+
order_id = await test_create_order(client, user_token, test_product)
|
| 70 |
+
|
| 71 |
+
# Test getting the order
|
| 72 |
+
response = await client.get(
|
| 73 |
+
f"/api/v1/orders/{order_id}",
|
| 74 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 75 |
+
)
|
| 76 |
+
assert response.status_code == 200
|
| 77 |
+
data = response.json()
|
| 78 |
+
assert data["id"] == order_id
|
| 79 |
+
assert len(data["items"]) == 1
|
| 80 |
+
|
| 81 |
+
async def test_list_orders(client: AsyncClient, user_token: str, test_product: dict):
|
| 82 |
+
# Create multiple orders
|
| 83 |
+
await test_create_order(client, user_token, test_product)
|
| 84 |
+
await test_create_order(client, user_token, test_product)
|
| 85 |
+
|
| 86 |
+
# Test listing orders
|
| 87 |
+
response = await client.get(
|
| 88 |
+
"/api/v1/orders/",
|
| 89 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 90 |
+
)
|
| 91 |
+
assert response.status_code == 200
|
| 92 |
+
data = response.json()
|
| 93 |
+
assert len(data) >= 2
|
| 94 |
+
|
| 95 |
+
async def test_update_order_status(client: AsyncClient, user_token: str, test_product: dict):
|
| 96 |
+
# First create an order
|
| 97 |
+
order_id = await test_create_order(client, user_token, test_product)
|
| 98 |
+
|
| 99 |
+
# Test updating the order status
|
| 100 |
+
response = await client.put(
|
| 101 |
+
f"/api/v1/orders/{order_id}/status",
|
| 102 |
+
json={"status": "processing"},
|
| 103 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 104 |
+
)
|
| 105 |
+
assert response.status_code == 200
|
| 106 |
+
data = response.json()
|
| 107 |
+
assert data["status"] == "processing"
|
| 108 |
+
|
| 109 |
+
# Test invalid status
|
| 110 |
+
response = await client.put(
|
| 111 |
+
f"/api/v1/orders/{order_id}/status",
|
| 112 |
+
json={"status": "invalid_status"},
|
| 113 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 114 |
+
)
|
| 115 |
+
assert response.status_code == 400
|
| 116 |
+
|
| 117 |
+
async def test_cancel_order(client: AsyncClient, user_token: str, test_product: dict):
|
| 118 |
+
# First create an order
|
| 119 |
+
order_id = await test_create_order(client, user_token, test_product)
|
| 120 |
+
|
| 121 |
+
# Get initial product inventory
|
| 122 |
+
product_response = await client.get(
|
| 123 |
+
f"/api/v1/products/{test_product['id']}",
|
| 124 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 125 |
+
)
|
| 126 |
+
initial_inventory = product_response.json()["inventory_count"]
|
| 127 |
+
|
| 128 |
+
# Cancel the order
|
| 129 |
+
response = await client.put(
|
| 130 |
+
f"/api/v1/orders/{order_id}/status",
|
| 131 |
+
json={"status": "cancelled"},
|
| 132 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 133 |
+
)
|
| 134 |
+
assert response.status_code == 200
|
| 135 |
+
assert response.json()["status"] == "cancelled"
|
| 136 |
+
|
| 137 |
+
# Verify inventory was restored
|
| 138 |
+
product_response = await client.get(
|
| 139 |
+
f"/api/v1/products/{test_product['id']}",
|
| 140 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 141 |
+
)
|
| 142 |
+
final_inventory = product_response.json()["inventory_count"]
|
| 143 |
+
assert final_inventory == initial_inventory + 2 # We ordered 2 items
|
tests/api/test_products.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from httpx import AsyncClient
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
+
from app.core.security import create_access_token
|
| 5 |
+
|
| 6 |
+
pytestmark = pytest.mark.asyncio
|
| 7 |
+
|
| 8 |
+
@pytest.fixture
|
| 9 |
+
async def user_token(client: AsyncClient) -> str:
|
| 10 |
+
"""Create a test user and return their token"""
|
| 11 |
+
# Register user
|
| 12 |
+
await client.post(
|
| 13 |
+
"/api/v1/auth/register",
|
| 14 |
+
data={
|
| 15 |
+
"username": "testuser@example.com",
|
| 16 |
+
"password": "testpass123",
|
| 17 |
+
"full_name": "Test User"
|
| 18 |
+
}
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Login to get token
|
| 22 |
+
response = await client.post(
|
| 23 |
+
"/api/v1/auth/login",
|
| 24 |
+
data={
|
| 25 |
+
"username": "testuser@example.com",
|
| 26 |
+
"password": "testpass123"
|
| 27 |
+
}
|
| 28 |
+
)
|
| 29 |
+
return response.json()["access_token"]
|
| 30 |
+
|
| 31 |
+
async def test_create_product(client: AsyncClient, user_token: str):
|
| 32 |
+
# Test creating a product
|
| 33 |
+
response = await client.post(
|
| 34 |
+
"/api/v1/products/",
|
| 35 |
+
json={
|
| 36 |
+
"name": "Test Product",
|
| 37 |
+
"description": "A test product",
|
| 38 |
+
"price": 99.99,
|
| 39 |
+
"category": "test",
|
| 40 |
+
"inventory_count": 10
|
| 41 |
+
},
|
| 42 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 43 |
+
)
|
| 44 |
+
assert response.status_code == 200
|
| 45 |
+
data = response.json()
|
| 46 |
+
assert data["name"] == "Test Product"
|
| 47 |
+
assert data["price"] == 99.99
|
| 48 |
+
return data["id"]
|
| 49 |
+
|
| 50 |
+
async def test_get_product(client: AsyncClient, user_token: str):
|
| 51 |
+
# First create a product
|
| 52 |
+
product_id = await test_create_product(client, user_token)
|
| 53 |
+
|
| 54 |
+
# Test getting the product
|
| 55 |
+
response = await client.get(
|
| 56 |
+
f"/api/v1/products/{product_id}",
|
| 57 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 58 |
+
)
|
| 59 |
+
assert response.status_code == 200
|
| 60 |
+
data = response.json()
|
| 61 |
+
assert data["id"] == product_id
|
| 62 |
+
assert data["name"] == "Test Product"
|
| 63 |
+
|
| 64 |
+
async def test_list_products(client: AsyncClient, user_token: str):
|
| 65 |
+
# Create multiple products
|
| 66 |
+
await test_create_product(client, user_token)
|
| 67 |
+
await client.post(
|
| 68 |
+
"/api/v1/products/",
|
| 69 |
+
json={
|
| 70 |
+
"name": "Another Product",
|
| 71 |
+
"description": "Another test product",
|
| 72 |
+
"price": 49.99,
|
| 73 |
+
"category": "test",
|
| 74 |
+
"inventory_count": 5
|
| 75 |
+
},
|
| 76 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Test listing products
|
| 80 |
+
response = await client.get(
|
| 81 |
+
"/api/v1/products/",
|
| 82 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 83 |
+
)
|
| 84 |
+
assert response.status_code == 200
|
| 85 |
+
data = response.json()
|
| 86 |
+
assert len(data) >= 2
|
| 87 |
+
assert any(p["name"] == "Test Product" for p in data)
|
| 88 |
+
assert any(p["name"] == "Another Product" for p in data)
|
| 89 |
+
|
| 90 |
+
async def test_update_product(client: AsyncClient, user_token: str):
|
| 91 |
+
# First create a product
|
| 92 |
+
product_id = await test_create_product(client, user_token)
|
| 93 |
+
|
| 94 |
+
# Test updating the product
|
| 95 |
+
response = await client.put(
|
| 96 |
+
f"/api/v1/products/{product_id}",
|
| 97 |
+
json={
|
| 98 |
+
"name": "Updated Product",
|
| 99 |
+
"description": "Updated description",
|
| 100 |
+
"price": 149.99,
|
| 101 |
+
"category": "test",
|
| 102 |
+
"inventory_count": 20
|
| 103 |
+
},
|
| 104 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 105 |
+
)
|
| 106 |
+
assert response.status_code == 200
|
| 107 |
+
data = response.json()
|
| 108 |
+
assert data["name"] == "Updated Product"
|
| 109 |
+
assert data["price"] == 149.99
|
| 110 |
+
assert data["inventory_count"] == 20
|
| 111 |
+
|
| 112 |
+
async def test_delete_product(client: AsyncClient, user_token: str):
|
| 113 |
+
# First create a product
|
| 114 |
+
product_id = await test_create_product(client, user_token)
|
| 115 |
+
|
| 116 |
+
# Test deleting the product
|
| 117 |
+
response = await client.delete(
|
| 118 |
+
f"/api/v1/products/{product_id}",
|
| 119 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 120 |
+
)
|
| 121 |
+
assert response.status_code == 200
|
| 122 |
+
|
| 123 |
+
# Verify product is deleted
|
| 124 |
+
response = await client.get(
|
| 125 |
+
f"/api/v1/products/{product_id}",
|
| 126 |
+
headers={"Authorization": f"Bearer {user_token}"}
|
| 127 |
+
)
|
| 128 |
+
assert response.status_code == 404
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pytest
|
| 3 |
+
import asyncio
|
| 4 |
+
from typing import AsyncGenerator, Generator
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
from httpx import AsyncClient
|
| 7 |
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
| 8 |
+
from sqlalchemy.orm import sessionmaker
|
| 9 |
+
from sqlalchemy.pool import StaticPool
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# Load test environment variables
|
| 14 |
+
test_env_path = Path(__file__).parent / "test.env"
|
| 15 |
+
load_dotenv(test_env_path)
|
| 16 |
+
|
| 17 |
+
from app.db.database import Base, get_db
|
| 18 |
+
from app.main import app
|
| 19 |
+
from app.core.config import settings
|
| 20 |
+
|
| 21 |
+
# Use SQLite for testing
|
| 22 |
+
SQLALCHEMY_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
| 23 |
+
|
| 24 |
+
engine = create_async_engine(
|
| 25 |
+
SQLALCHEMY_DATABASE_URL,
|
| 26 |
+
connect_args={"check_same_thread": False},
|
| 27 |
+
poolclass=StaticPool,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
TestingSessionLocal = sessionmaker(
|
| 31 |
+
engine,
|
| 32 |
+
class_=AsyncSession,
|
| 33 |
+
expire_on_commit=False,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
@pytest.fixture(scope="session")
|
| 37 |
+
def event_loop() -> Generator:
|
| 38 |
+
"""Create an instance of the default event loop for each test case."""
|
| 39 |
+
try:
|
| 40 |
+
loop = asyncio.get_event_loop()
|
| 41 |
+
except RuntimeError:
|
| 42 |
+
loop = asyncio.new_event_loop()
|
| 43 |
+
yield loop
|
| 44 |
+
loop.close()
|
| 45 |
+
|
| 46 |
+
@pytest.fixture(scope="session")
|
| 47 |
+
async def test_app():
|
| 48 |
+
"""Create tables for testing and clean up afterward."""
|
| 49 |
+
# Create test upload directories
|
| 50 |
+
os.makedirs("./test_uploads/documents", exist_ok=True)
|
| 51 |
+
os.makedirs("./test_uploads/images", exist_ok=True)
|
| 52 |
+
|
| 53 |
+
# Create tables
|
| 54 |
+
async with engine.begin() as conn:
|
| 55 |
+
await conn.run_sync(Base.metadata.drop_all) # Ensure clean state
|
| 56 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 57 |
+
|
| 58 |
+
yield app
|
| 59 |
+
|
| 60 |
+
# Cleanup
|
| 61 |
+
async with engine.begin() as conn:
|
| 62 |
+
await conn.run_sync(Base.metadata.drop_all)
|
| 63 |
+
|
| 64 |
+
@pytest.fixture
|
| 65 |
+
async def db_session() -> AsyncGenerator[AsyncSession, None]:
|
| 66 |
+
"""Get a test database session."""
|
| 67 |
+
async with TestingSessionLocal() as session:
|
| 68 |
+
yield session
|
| 69 |
+
# Rollback any changes made in the test
|
| 70 |
+
await session.rollback()
|
| 71 |
+
|
| 72 |
+
@pytest.fixture
|
| 73 |
+
async def client(test_app) -> AsyncGenerator[AsyncClient, None]:
|
| 74 |
+
"""Get a test client with database session override."""
|
| 75 |
+
async def override_get_db():
|
| 76 |
+
async with TestingSessionLocal() as session:
|
| 77 |
+
yield session
|
| 78 |
+
|
| 79 |
+
app.dependency_overrides[get_db] = override_get_db
|
| 80 |
+
|
| 81 |
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
| 82 |
+
yield client
|
| 83 |
+
|
| 84 |
+
app.dependency_overrides.clear()
|
tests/test.env
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PROJECT_NAME=Admin Dashboard Test
|
| 2 |
+
VERSION=1.0.0
|
| 3 |
+
API_V1_STR=/api/v1
|
| 4 |
+
SECRET_KEY=testingsecretkey123notforproduction
|
| 5 |
+
ALGORITHM=HS256
|
| 6 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
| 7 |
+
|
| 8 |
+
# Database settings
|
| 9 |
+
DATABASE_URL=sqlite+aiosqlite:///:memory:
|
| 10 |
+
|
| 11 |
+
# Redis settings (using dummy values for testing)
|
| 12 |
+
REDIS_HOST=localhost
|
| 13 |
+
REDIS_PORT=6379
|
| 14 |
+
|
| 15 |
+
# Email settings (using dummy values for testing)
|
| 16 |
+
SMTP_HOST=localhost
|
| 17 |
+
SMTP_PORT=587
|
| 18 |
+
SMTP_USER=test@example.com
|
| 19 |
+
SMTP_PASSWORD=dummypassword
|
| 20 |
+
EMAILS_FROM_EMAIL=test@example.com
|
| 21 |
+
EMAILS_FROM_NAME=Test System
|
| 22 |
+
|
| 23 |
+
# Storage settings
|
| 24 |
+
MAX_UPLOAD_DIR_SIZE_MB=1024
|
| 25 |
+
UPLOAD_DIR=./test_uploads
|
| 26 |
+
MAX_DB_CONNECTIONS=100
|
tests/utils.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
from httpx import AsyncClient
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
async def create_user_and_login(
|
| 6 |
+
client: AsyncClient,
|
| 7 |
+
email: str = "testuser@example.com",
|
| 8 |
+
password: str = "testpass123",
|
| 9 |
+
full_name: str = "Test User",
|
| 10 |
+
is_superuser: bool = False
|
| 11 |
+
) -> Dict[str, Any]:
|
| 12 |
+
"""Create a test user and return their token and data"""
|
| 13 |
+
# Register user
|
| 14 |
+
register_response = await client.post(
|
| 15 |
+
"/api/v1/auth/register",
|
| 16 |
+
json={
|
| 17 |
+
"email": email,
|
| 18 |
+
"password": password,
|
| 19 |
+
"full_name": full_name,
|
| 20 |
+
"is_superuser": is_superuser
|
| 21 |
+
}
|
| 22 |
+
)
|
| 23 |
+
user_data = register_response.json()
|
| 24 |
+
|
| 25 |
+
# Login to get token
|
| 26 |
+
login_response = await client.post(
|
| 27 |
+
"/api/v1/auth/login",
|
| 28 |
+
data={
|
| 29 |
+
"username": email,
|
| 30 |
+
"password": password
|
| 31 |
+
}
|
| 32 |
+
)
|
| 33 |
+
token_data = login_response.json()
|
| 34 |
+
|
| 35 |
+
return {
|
| 36 |
+
"token": token_data["access_token"],
|
| 37 |
+
"user": user_data
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
async def create_branch(
|
| 41 |
+
client: AsyncClient,
|
| 42 |
+
token: str,
|
| 43 |
+
name: str = "Test Branch",
|
| 44 |
+
location: str = "Test Location"
|
| 45 |
+
) -> Dict[str, Any]:
|
| 46 |
+
"""Create a test branch and return its data"""
|
| 47 |
+
response = await client.post(
|
| 48 |
+
"/api/v1/branches/",
|
| 49 |
+
json={
|
| 50 |
+
"name": name,
|
| 51 |
+
"location": location
|
| 52 |
+
},
|
| 53 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 54 |
+
)
|
| 55 |
+
return response.json()
|
| 56 |
+
|
| 57 |
+
async def create_product(
|
| 58 |
+
client: AsyncClient,
|
| 59 |
+
token: str,
|
| 60 |
+
branch_id: int,
|
| 61 |
+
name: str = "Test Product",
|
| 62 |
+
price: float = 99.99,
|
| 63 |
+
inventory: int = 10
|
| 64 |
+
) -> Dict[str, Any]:
|
| 65 |
+
"""Create a test product and return its data"""
|
| 66 |
+
response = await client.post(
|
| 67 |
+
"/api/v1/products/",
|
| 68 |
+
json={
|
| 69 |
+
"name": name,
|
| 70 |
+
"description": f"Description for {name}",
|
| 71 |
+
"price": price,
|
| 72 |
+
"category": "test",
|
| 73 |
+
"inventory_count": inventory,
|
| 74 |
+
"branch_id": branch_id
|
| 75 |
+
},
|
| 76 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 77 |
+
)
|
| 78 |
+
return response.json()
|
| 79 |
+
|
| 80 |
+
async def create_order(
|
| 81 |
+
client: AsyncClient,
|
| 82 |
+
token: str,
|
| 83 |
+
items: list,
|
| 84 |
+
branch_id: int
|
| 85 |
+
) -> Dict[str, Any]:
|
| 86 |
+
"""Create a test order and return its data"""
|
| 87 |
+
response = await client.post(
|
| 88 |
+
"/api/v1/orders/",
|
| 89 |
+
json={
|
| 90 |
+
"items": items,
|
| 91 |
+
"branch_id": branch_id
|
| 92 |
+
},
|
| 93 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 94 |
+
)
|
| 95 |
+
return response.json()
|
| 96 |
+
|
| 97 |
+
def assert_response_matches_schema(response_data: Dict[str, Any], schema: Dict[str, Any]):
|
| 98 |
+
"""Assert that an API response matches its expected schema"""
|
| 99 |
+
assert all(key in response_data for key in schema.keys()), \
|
| 100 |
+
f"Missing required fields. Expected {schema.keys()}, got {response_data.keys()}"
|
| 101 |
+
|
| 102 |
+
for key, value_type in schema.items():
|
| 103 |
+
assert isinstance(response_data[key], value_type), \
|
| 104 |
+
f"Field {key} should be of type {value_type}, got {type(response_data[key])}"
|