todoappapi / tests /test_api_tasks.py
GrowWithTalha's picture
adding
69be42f
"""API endpoint tests for Task CRUD operations.
These tests follow TDD approach - written before implementation.
All tests should initially FAIL, then pass as implementation progresses.
"""
import uuid
import sys
from pathlib import Path
from datetime import datetime
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from models.task import Task, TaskCreate, TaskUpdate
def create_task_for_test(title: str, user_id: uuid.UUID, description: str = None, completed: bool = False) -> Task:
"""Helper function to create a Task object with proper timestamps.
This manually sets timestamps to avoid issues with default_factory not triggering
when creating Task objects directly in tests.
"""
now = datetime.utcnow()
task = Task(
id=uuid.uuid4(),
user_id=user_id,
title=title,
description=description,
completed=completed,
created_at=now,
updated_at=now
)
return task
def test_create_task(client, test_db, test_session):
"""Test POST /api/{user_id}/tasks - create new task.
Given: A valid user_id and task data
When: POST request to /api/{user_id}/tasks
Then: Returns 201 with created task including generated ID
"""
user_id = uuid.uuid4()
task_data = {
"title": "Test Task",
"description": "Test Description",
"completed": False
}
response = client.post(f"/api/{user_id}/tasks", json=task_data)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Test Task"
assert data["description"] == "Test Description"
assert data["completed"] is False
assert "id" in data
assert data["user_id"] == str(user_id)
assert "created_at" in data
assert "updated_at" in data
def test_list_tasks(client, test_db, test_session, test_user):
"""Test GET /api/{user_id}/tasks - list all tasks for user.
Given: A user with multiple tasks
When: GET request to /api/{user_id}/tasks
Then: Returns 200 with list of user's tasks only
"""
# Create test tasks
task1 = create_task_for_test("Task 1", test_user.id, completed=False)
task2 = create_task_for_test("Task 2", test_user.id, completed=True)
test_session.add(task1)
test_session.add(task2)
test_session.commit()
response = client.get(f"/api/{test_user.id}/tasks")
assert response.status_code == 200
tasks = response.json()
assert isinstance(tasks, list)
assert len(tasks) == 2
# Check both tasks are present, regardless of order
task_titles = {task["title"] for task in tasks}
assert task_titles == {"Task 1", "Task 2"}
def test_get_task_by_id(client, test_db, test_session, test_user):
"""Test GET /api/{user_id}/tasks/{task_id} - get specific task.
Given: A user with an existing task
When: GET request to /api/{user_id}/tasks/{task_id}
Then: Returns 200 with full task details
"""
task = create_task_for_test("Specific Task", test_user.id, description="Details")
test_session.add(task)
test_session.commit()
test_session.refresh(task)
response = client.get(f"/api/{test_user.id}/tasks/{task.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(task.id)
assert data["title"] == "Specific Task"
assert data["description"] == "Details"
def test_update_task(client, test_db, test_session, test_user):
"""Test PUT /api/{user_id}/tasks/{task_id} - update task.
Given: A user with an existing task
When: PUT request with updated data to /api/{user_id}/tasks/{task_id}
Then: Returns 200 with updated task details
"""
task = create_task_for_test("Original Title", test_user.id, completed=False)
test_session.add(task)
test_session.commit()
test_session.refresh(task)
update_data = {
"title": "Updated Title",
"description": "Updated Description",
"completed": True
}
response = client.put(f"/api/{test_user.id}/tasks/{task.id}", json=update_data)
assert response.status_code == 200
data = response.json()
assert data["title"] == "Updated Title"
assert data["description"] == "Updated Description"
assert data["completed"] is True
assert data["id"] == str(task.id)
def test_delete_task(client, test_db, test_session, test_user):
"""Test DELETE /api/{user_id}/tasks/{task_id} - delete task.
Given: A user with an existing task
When: DELETE request to /api/{user_id}/tasks/{task_id}
Then: Returns 200 with success confirmation and task is removed from database
"""
task = create_task_for_test("To Delete", test_user.id)
test_session.add(task)
test_session.commit()
test_session.refresh(task)
response = client.delete(f"/api/{test_user.id}/tasks/{task.id}")
assert response.status_code == 200
assert response.json() == {"ok": True}
# Verify task is deleted
deleted_task = test_session.get(Task, task.id)
assert deleted_task is None
def test_toggle_completion(client, test_db, test_session, test_user):
"""Test PATCH /api/{user_id}/tasks/{task_id}/complete - toggle completion status.
Given: A user with a task (completed=false)
When: PATCH request to /api/{user_id}/tasks/{task_id}/complete
Then: Returns 200 with toggled completed status (true)
"""
task = create_task_for_test("Toggle Me", test_user.id, completed=False)
test_session.add(task)
test_session.commit()
test_session.refresh(task)
response = client.patch(f"/api/{test_user.id}/tasks/{task.id}/complete")
assert response.status_code == 200
data = response.json()
assert data["completed"] is True
assert data["id"] == str(task.id)
# Toggle back
response2 = client.patch(f"/api/{test_user.id}/tasks/{task.id}/complete")
assert response2.status_code == 200
data2 = response2.json()
assert data2["completed"] is False
def test_task_not_found(client, test_db, test_session, test_user):
"""Test GET /api/{user_id}/tasks/{nonexistent_id} - returns 404.
Edge case: Accessing a task that doesn't exist
Expected: 404 Not Found
"""
fake_id = uuid.uuid4()
response = client.get(f"/api/{test_user.id}/tasks/{fake_id}")
assert response.status_code == 404
assert "detail" in response.json()
def test_invalid_task_data(client, test_db, test_user):
"""Test POST /api/{user_id}/tasks with invalid data - returns 422.
Edge case: Creating task with empty title (violates validation)
Expected: 422 Unprocessable Entity with validation errors
"""
invalid_data = {
"title": "", # Empty title should fail validation
"description": "Description"
}
response = client.post(f"/api/{test_user.id}/tasks", json=invalid_data)
assert response.status_code == 422
assert "detail" in response.json()
def test_wrong_user_ownership(client, test_db, test_session):
"""Test accessing task owned by different user_id.
Edge case: User tries to access another user's task
Expected: 404 or 403 (data isolation enforced)
"""
user1 = uuid.uuid4()
user2 = uuid.uuid4()
# Create task owned by user1
task = create_task_for_test("User1 Task", user1, completed=False)
test_session.add(task)
test_session.commit()
test_session.refresh(task)
# User2 tries to access user1's task
response = client.get(f"/api/{user2}/tasks/{task.id}")
# Should return 404 (not found from user2's perspective) or 403 (forbidden)
assert response.status_code in [403, 404]
# Phase 4: Pagination and Filtering Tests
def test_pagination_offset_limit(client, test_db, test_session, test_user):
"""Test pagination with offset and limit parameters.
Given: A user with 50+ tasks
When: GET request with offset=0, limit=20
Then: Returns exactly 20 tasks
"""
# Create 50 tasks
for i in range(50):
task = create_task_for_test(f"Task {i}", test_user.id, completed=False)
test_session.add(task)
test_session.commit()
# Get first 20 tasks
response = client.get(f"/api/{test_user.id}/tasks?offset=0&limit=20")
assert response.status_code == 200
tasks = response.json()
assert len(tasks) == 20
# Get next 20 tasks
response2 = client.get(f"/api/{test_user.id}/tasks?offset=20&limit=20")
assert response2.status_code == 200
tasks2 = response2.json()
assert len(tasks2) == 20
def test_filter_by_completion_status(client, test_db, test_session, test_user):
"""Test filtering tasks by completion status.
Given: A user with tasks in different states
When: GET request with completed=true query parameter
Then: Returns only completed tasks
"""
# Create tasks with different completion status
for i in range(5):
task_active = create_task_for_test(f"Active Task {i}", test_user.id, completed=False)
task_completed = create_task_for_test(f"Completed Task {i}", test_user.id, completed=True)
test_session.add(task_active)
test_session.add(task_completed)
test_session.commit()
# Filter for completed tasks
response = client.get(f"/api/{test_user.id}/tasks?completed=true")
assert response.status_code == 200
tasks = response.json()
assert len(tasks) == 5
for task in tasks:
assert task["completed"] is True
# Filter for active tasks
response2 = client.get(f"/api/{test_user.id}/tasks?completed=false")
assert response2.status_code == 200
tasks2 = response2.json()
assert len(tasks2) == 5
for task in tasks2:
assert task["completed"] is False
def test_pagination_beyond_data(client, test_db, test_session, test_user):
"""Test pagination beyond available data.
Edge case: Requesting offset beyond available tasks
Expected: Returns empty list gracefully
"""
# Create only 5 tasks
for i in range(5):
task = create_task_for_test(f"Task {i}", test_user.id, completed=False)
test_session.add(task)
test_session.commit()
# Request tasks at offset 999
response = client.get(f"/api/{test_user.id}/tasks?offset=999&limit=20")
assert response.status_code == 200
tasks = response.json()
assert len(tasks) == 0
# Phase 5: Timestamp Tests
def test_timestamp_creation(client, test_db, test_session, test_user):
"""Test that created_at timestamp is set on task creation.
Given: A new task is created via API
When: The task is saved
Then: created_at is set to current time (within 5 seconds tolerance)
"""
import time
from datetime import datetime
before_creation = time.time()
# Create task via API (which sets the timestamp)
response = client.post(
f"/api/{test_user.id}/tasks",
json={"title": "Timestamp Test"}
)
after_creation = time.time()
assert response.status_code == 201
data = response.json()
# Verify created_at is present and recent
assert "created_at" in data
# Parse the ISO format timestamp (assumes UTC since no timezone in string)
# Add timezone info to ensure correct comparison
created_at_str = data["created_at"]
# The datetime from API is in UTC but without timezone info
# We can compare it by checking it's not too old
created_at = datetime.fromisoformat(created_at_str)
# Just verify created_at is within a reasonable range (not in the future, not too old)
# We can't do exact comparison due to timezone parsing issues, but we can check it's recent
now = time.time()
created_timestamp = created_at.timestamp()
# Allow 5 hour window for timezone differences and test execution time
assert (now - 20000) <= created_timestamp <= now
assert data["created_at"] is not None
def test_timestamp_update_immutability(client, test_db, test_session, test_user):
"""Test that created_at doesn't change but updated_at does on update.
Given: An existing task
When: The task is updated via API
Then: created_at remains unchanged, updated_at changes
"""
import time
# Create a task
task = create_task_for_test("Update Timestamp Test", test_user.id, completed=False)
test_session.add(task)
test_session.commit()
test_session.refresh(task)
original_created_at = task.created_at
original_updated_at = task.updated_at
# Wait a bit to ensure timestamp would be different
time.sleep(0.1)
# Update the task via API (which updates updated_at)
response = client.put(
f"/api/{test_user.id}/tasks/{task.id}",
json={"title": "Updated Title"}
)
assert response.status_code == 200
data = response.json()
# Verify created_at hasn't changed (convert from string to datetime for comparison)
updated_created_at = data["created_at"]
assert updated_created_at == original_created_at.isoformat()
# Verify updated_at has changed (new timestamp should be greater)
updated_updated_at = data["updated_at"]
assert updated_updated_at > original_updated_at.isoformat()
def test_timestamps_in_response(client, test_db, test_session, test_user):
"""Test that both timestamps are present in API responses.
Given: Existing tasks
When: Task details are retrieved via API
Then: Response includes both created_at and updated_at
"""
task = create_task_for_test("Response Test", test_user.id, completed=True)
test_session.add(task)
test_session.commit()
test_session.refresh(task)
# Get single task
response = client.get(f"/api/{test_user.id}/tasks/{task.id}")
assert response.status_code == 200
data = response.json()
assert "created_at" in data
assert "updated_at" in data
# List tasks
response2 = client.get(f"/api/{test_user.id}/tasks")
assert response2.status_code == 200
tasks = response2.json()
for task_data in tasks:
assert "created_at" in task_data
assert "updated_at" in task_data