import pytest
import httpx
import asyncio
import os
import random
import string
from typing import Dict, Any, List
# Configuration for test environment
BASE_URL = "http://localhost:8000" # Adjust if needed
TEST_FILE_DIR = "./test_uploads"
# Utility Functions
def generate_random_string(length: int = 10) -> str:
"""Generate a random string for testing."""
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
def generate_test_note(tags: List[str] = None, attachments: List[Dict] = None) -> Dict[str, Any]:
"""Generate a test note with optional tags and attachments."""
return {
"title": f"Test Note {generate_random_string()}",
"body": f"This is a test note content {generate_random_string(20)}",
"tags": tags or [generate_random_string(5)],
"attachments": attachments or []
}
@pytest.fixture
async def async_client():
"""Create an async HTTP client for testing."""
async with httpx.AsyncClient(base_url=BASE_URL) as client:
yield client
@pytest.fixture
def test_file():
"""Create a test file for upload."""
os.makedirs(TEST_FILE_DIR, exist_ok=True)
test_file_path = os.path.join(TEST_FILE_DIR, f"test_file_{generate_random_string()}.txt")
with open(test_file_path, 'w') as f:
f.write(f"Test file content {generate_random_string(20)}")
yield test_file_path
# Cleanup
os.remove(test_file_path)
@pytest.mark.asyncio
async def test_health_check(async_client):
"""Test the health check endpoint."""
response = await async_client.get("/api/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.asyncio
async def test_create_note_basic(async_client):
"""Test creating a basic note."""
note_data = generate_test_note()
response = await async_client.post("/api/notes", json=note_data)
assert response.status_code == 200
created_note = response.json()
assert created_note["title"] == note_data["title"]
assert created_note["body"] == note_data["body"]
assert set(created_note["tags"]) == set(note_data["tags"])
assert "id" in created_note
assert "created_at" in created_note
assert "updated_at" in created_note
@pytest.mark.asyncio
async def test_create_note_with_multiple_tags(async_client):
"""Test creating a note with multiple tags."""
note_data = generate_test_note(
tags=["work", "personal", generate_random_string(5)]
)
response = await async_client.post("/api/notes", json=note_data)
assert response.status_code == 200
created_note = response.json()
assert set(created_note["tags"]) == set(note_data["tags"])
@pytest.mark.asyncio
async def test_update_note(async_client):
"""Test updating an existing note."""
# First create a note
initial_note = generate_test_note()
create_response = await async_client.post("/api/notes", json=initial_note)
created_note = create_response.json()
# Now update the note
update_data = {
"title": f"Updated {created_note['title']}",
"body": f"Updated {created_note['body']}",
"tags": ["updated", "test"]
}
update_response = await async_client.put(f"/api/notes/{created_note['id']}", json=update_data)
assert update_response.status_code == 200
updated_note = update_response.json()
assert updated_note["title"] == update_data["title"]
assert updated_note["body"] == update_data["body"]
assert set(updated_note["tags"]) == set(update_data["tags"])
@pytest.mark.asyncio
async def test_file_upload(async_client, test_file):
"""Test file upload functionality."""
with open(test_file, 'rb') as f:
files = {'file': f}
response = await async_client.post("/api/upload", files=files)
assert response.status_code == 200
upload_result = response.json()
assert "url" in upload_result
assert "type" in upload_result
assert upload_result["type"] in ["file", "image"]
assert "/static/" in upload_result["url"]
@pytest.mark.asyncio
async def test_note_with_attachments(async_client, test_file):
"""Test creating a note with file attachments."""
# First upload a file
with open(test_file, 'rb') as f:
files = {'file': f}
upload_response = await async_client.post("/api/upload", files=files)
file_upload = upload_response.json()
# Create note with attachment
note_data = generate_test_note(
attachments=[{
"url": file_upload["url"],
"type": file_upload["type"]
}]
)
response = await async_client.post("/api/notes", json=note_data)
assert response.status_code == 200
created_note = response.json()
assert len(created_note["attachments"]) == 1
assert created_note["attachments"][0]["url"] == file_upload["url"]
@pytest.mark.asyncio
async def test_list_notes_with_tag_filter(async_client):
"""Test listing notes with tag filtering."""
unique_tag = generate_random_string(5)
# Create multiple notes with the same tag
for _ in range(3):
note_data = generate_test_note(tags=[unique_tag])
await async_client.post("/api/notes", json=note_data)
# List notes with the tag
response = await async_client.get(f"/api/notes?tag={unique_tag}")
assert response.status_code == 200
notes = response.json()
assert len(notes) >= 3
assert all(unique_tag in note["tags"] for note in notes)
@pytest.mark.asyncio
async def test_list_tags(async_client):
"""Test listing all tags."""
# Create some notes with unique tags
unique_tags = [generate_random_string(5) for _ in range(5)]
for tag in unique_tags:
note_data = generate_test_note(tags=[tag])
await async_client.post("/api/notes", json=note_data)
# List tags
response = await async_client.get("/api/tags")
assert response.status_code == 200
tags = response.json()
# Check that all unique tags are present
assert all({"name": tag} in tags for tag in unique_tags)
@pytest.mark.asyncio
async def test_concurrent_note_creation(async_client):
"""Test concurrent note creation to check for race conditions."""
async def create_note():
note_data = generate_test_note()
return await async_client.post("/api/notes", json=note_data)
# Create 10 notes concurrently
responses = await asyncio.gather(*[create_note() for _ in range(10)])
# Check all notes were created successfully
assert all(response.status_code == 200 for response in responses)
# Ensure unique notes
note_ids = [response.json()["id"] for response in responses]
assert len(set(note_ids)) == 10
# Performance and Stress Tests
@pytest.mark.asyncio
async def test_create_many_notes(async_client):
"""Stress test: Create a large number of notes."""
async def create_note():
note_data = generate_test_note()
return await async_client.post("/api/notes", json=note_data)
# Create 100 notes
responses = await asyncio.gather(*[create_note() for _ in range(100)])
assert all(response.status_code == 200 for response in responses)
# Edge Cases and Error Handling
@pytest.mark.asyncio
async def test_update_nonexistent_note(async_client):
"""Test updating a non-existent note."""
non_existent_id = 999999 # Assuming this ID doesn't exist
update_data = generate_test_note()
response = await async_client.put(f"/api/notes/{non_existent_id}", json=update_data)
assert response.status_code == 404
# Bonus: Randomized Testing
@pytest.mark.asyncio
async def test_random_note_operations(async_client):
"""Perform a series of randomized note operations."""
operations = []
# Create some initial notes
for _ in range(5):
note_data = generate_test_note()
response = await async_client.post("/api/notes", json=note_data)
operations.append(("create", response.json()))
# Perform random updates and deletions (simulated)
for _ in range(10):
if operations and random.random() < 0.5:
# Randomly select a note to update
note = random.choice(operations)
if note[0] == "create":
update_data = generate_test_note()
response = await async_client.put(f"/api/notes/{note[1]['id']}", json=update_data)
assert response.status_code == 200
# Configuration for running tests
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])