feat: add comprehensive pytest test suite
Implemented full test coverage for existing features: - Unit tests (18 tests): Validation logic for email, phone, firstName - Integration tests (29 tests): Member model, GraphQL queries/mutations - E2E tests (5 tests): Complete GraphQL API flows over HTTP - MCP server tests (12 tests): All 6 MCP tools Test organization: - tests/unit/ - Pure logic tests - tests/integration/ - Database and resolver tests - tests/e2e/ - Full API request/response tests - tests/mcp/ - MCP server tool tests - tests/conftest.py - Shared fixtures All 57 tests passing ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Integration tests for Member database model."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models.member import Member
|
||||
|
||||
|
||||
class TestMemberModel:
|
||||
"""Tests for Member SQLAlchemy model."""
|
||||
|
||||
async def test_create_member_with_minimal_data(self, async_session):
|
||||
"""Test creating a member with only firstName (minimal required data)."""
|
||||
member = Member(first_name="Alice")
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
assert member.id is not None
|
||||
assert member.first_name == "Alice"
|
||||
assert member.last_name is None
|
||||
assert member.street is None
|
||||
assert member.apartment_number is None
|
||||
assert member.zip is None
|
||||
assert member.city is None
|
||||
assert member.country is None
|
||||
assert member.email is None
|
||||
assert member.phone is None
|
||||
assert isinstance(member.created_at, datetime)
|
||||
assert isinstance(member.updated_at, datetime)
|
||||
|
||||
async def test_create_member_with_complete_data(self, async_session):
|
||||
"""Test creating a member with all fields populated."""
|
||||
member = Member(
|
||||
first_name="Bob",
|
||||
last_name="Johnson",
|
||||
street="123 Main St",
|
||||
apartment_number="4B",
|
||||
zip="12345",
|
||||
city="Springfield",
|
||||
country="USA",
|
||||
email="bob@example.com",
|
||||
phone="+14155551234",
|
||||
)
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
assert member.id is not None
|
||||
assert member.first_name == "Bob"
|
||||
assert member.last_name == "Johnson"
|
||||
assert member.street == "123 Main St"
|
||||
assert member.apartment_number == "4B"
|
||||
assert member.zip == "12345"
|
||||
assert member.city == "Springfield"
|
||||
assert member.country == "USA"
|
||||
assert member.email == "bob@example.com"
|
||||
assert member.phone == "+14155551234"
|
||||
|
||||
async def test_member_timestamps(self, async_session):
|
||||
"""Test that created_at and updated_at are set correctly."""
|
||||
member = Member(first_name="Charlie")
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
assert isinstance(member.created_at, datetime)
|
||||
assert isinstance(member.updated_at, datetime)
|
||||
# For new members, created_at should equal updated_at
|
||||
assert member.created_at == member.updated_at
|
||||
|
||||
async def test_member_repr(self, async_session):
|
||||
"""Test the __repr__ method of Member."""
|
||||
member = Member(first_name="David", last_name="Smith")
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
repr_str = repr(member)
|
||||
assert "Member" in repr_str
|
||||
assert f"id={member.id}" in repr_str
|
||||
assert "first_name='David'" in repr_str
|
||||
assert "last_name='Smith'" in repr_str
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Integration tests for GraphQL mutation resolvers."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.models.member import Member
|
||||
from src.resolvers.member import MemberNotFoundError, Mutation
|
||||
from src.schemas.member import CreateMemberInput, UpdateMemberInput
|
||||
from src.validation import ValidationError
|
||||
|
||||
|
||||
class TestCreateMemberMutation:
|
||||
"""Tests for createMember mutation."""
|
||||
|
||||
async def test_create_member_with_minimal_data(self, async_session, patched_session_maker):
|
||||
"""Test creating a member with only firstName."""
|
||||
mutation = Mutation()
|
||||
input_data = CreateMemberInput(first_name="Bob")
|
||||
|
||||
result = await mutation.create_member(input=input_data)
|
||||
|
||||
assert result.id is not None
|
||||
assert result.first_name == "Bob"
|
||||
assert result.last_name is None
|
||||
assert result.email is None
|
||||
assert result.phone is None
|
||||
|
||||
# Verify in database
|
||||
db_result = await async_session.execute(
|
||||
select(Member).where(Member.id == result.id)
|
||||
)
|
||||
db_member = db_result.scalar_one()
|
||||
assert db_member.first_name == "Bob"
|
||||
|
||||
async def test_create_member_with_complete_data(self, patched_session_maker):
|
||||
"""Test creating a member with all fields populated."""
|
||||
mutation = Mutation()
|
||||
input_data = CreateMemberInput(
|
||||
first_name="Alice",
|
||||
last_name="Johnson",
|
||||
street="456 Oak Ave",
|
||||
apartment_number="2A",
|
||||
zip="54321",
|
||||
city="Portland",
|
||||
country="USA",
|
||||
email="alice@example.com",
|
||||
phone="+14155559999",
|
||||
)
|
||||
|
||||
result = await mutation.create_member(input=input_data)
|
||||
|
||||
assert result.first_name == "Alice"
|
||||
assert result.last_name == "Johnson"
|
||||
assert result.street == "456 Oak Ave"
|
||||
assert result.apartment_number == "2A"
|
||||
assert result.zip == "54321"
|
||||
assert result.city == "Portland"
|
||||
assert result.country == "USA"
|
||||
assert result.email == "alice@example.com"
|
||||
assert result.phone == "+14155559999"
|
||||
|
||||
async def test_create_member_with_invalid_email(self, patched_session_maker):
|
||||
"""Test creating a member with invalid email raises ValidationError."""
|
||||
mutation = Mutation()
|
||||
input_data = CreateMemberInput(
|
||||
first_name="Charlie",
|
||||
email="not-an-email"
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match="Invalid email format"):
|
||||
await mutation.create_member(input=input_data)
|
||||
|
||||
async def test_create_member_with_invalid_phone(self, patched_session_maker):
|
||||
"""Test creating a member with invalid phone raises ValidationError."""
|
||||
mutation = Mutation()
|
||||
input_data = CreateMemberInput(
|
||||
first_name="David",
|
||||
phone="abc123" # Invalid characters
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match="Invalid phone format"):
|
||||
await mutation.create_member(input=input_data)
|
||||
|
||||
async def test_create_member_with_empty_first_name(self, patched_session_maker):
|
||||
"""Test creating a member with empty firstName raises ValidationError."""
|
||||
mutation = Mutation()
|
||||
input_data = CreateMemberInput(first_name="")
|
||||
|
||||
with pytest.raises(ValidationError, match="firstName cannot be empty"):
|
||||
await mutation.create_member(input=input_data)
|
||||
|
||||
|
||||
class TestUpdateMemberMutation:
|
||||
"""Tests for updateMember mutation."""
|
||||
|
||||
async def test_update_member_successfully(self, async_session, patched_session_maker):
|
||||
"""Test updating a member's fields successfully."""
|
||||
# Create a member
|
||||
member = Member(first_name="Eve", email="old@example.com")
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
original_created_at = member.created_at
|
||||
|
||||
# Update the member
|
||||
mutation = Mutation()
|
||||
input_data = UpdateMemberInput(
|
||||
id=member.id,
|
||||
email="new@example.com",
|
||||
phone="+14155551111"
|
||||
)
|
||||
|
||||
result = await mutation.update_member(input=input_data)
|
||||
|
||||
assert result.id == member.id
|
||||
assert result.first_name == "Eve" # Unchanged
|
||||
assert result.email == "new@example.com" # Updated
|
||||
assert result.phone == "+14155551111" # Updated
|
||||
|
||||
# Verify updated_at is set (can't reliably test it changed due to timing)
|
||||
assert result.updated_at is not None
|
||||
assert result.created_at == original_created_at
|
||||
|
||||
async def test_update_member_with_nonexistent_id(self, patched_session_maker):
|
||||
"""Test updating a non-existent member raises MemberNotFoundError."""
|
||||
mutation = Mutation()
|
||||
input_data = UpdateMemberInput(id=999, email="test@example.com")
|
||||
|
||||
with pytest.raises(MemberNotFoundError, match="Member with ID 999 not found"):
|
||||
await mutation.update_member(input=input_data)
|
||||
|
||||
async def test_update_member_partial_data(self, async_session, patched_session_maker):
|
||||
"""Test updating only some fields leaves others unchanged."""
|
||||
# Create a member with complete data
|
||||
member = Member(
|
||||
first_name="Frank",
|
||||
last_name="Miller",
|
||||
email="frank@example.com",
|
||||
phone="+14155552222"
|
||||
)
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
# Update only email
|
||||
mutation = Mutation()
|
||||
input_data = UpdateMemberInput(id=member.id, email="updated@example.com")
|
||||
|
||||
result = await mutation.update_member(input=input_data)
|
||||
|
||||
assert result.first_name == "Frank" # Unchanged
|
||||
assert result.last_name == "Miller" # Unchanged
|
||||
assert result.email == "updated@example.com" # Updated
|
||||
assert result.phone == "+14155552222" # Unchanged
|
||||
|
||||
async def test_update_member_with_invalid_email(self, async_session, patched_session_maker):
|
||||
"""Test updating with invalid email raises ValidationError."""
|
||||
# Create a member
|
||||
member = Member(first_name="Grace")
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
# Try to update with invalid email
|
||||
mutation = Mutation()
|
||||
input_data = UpdateMemberInput(id=member.id, email="invalid-email")
|
||||
|
||||
with pytest.raises(ValidationError, match="Invalid email format"):
|
||||
await mutation.update_member(input=input_data)
|
||||
|
||||
|
||||
class TestDeleteMemberMutation:
|
||||
"""Tests for deleteMember mutation."""
|
||||
|
||||
async def test_delete_member_successfully(self, async_session, patched_session_maker):
|
||||
"""Test deleting a member successfully."""
|
||||
# Create a member
|
||||
member = Member(first_name="Henry")
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
member_id = member.id
|
||||
|
||||
# Delete the member
|
||||
mutation = Mutation()
|
||||
result = await mutation.delete_member(id=member_id)
|
||||
|
||||
assert result is True
|
||||
|
||||
# Verify member is gone
|
||||
db_result = await async_session.execute(
|
||||
select(Member).where(Member.id == member_id)
|
||||
)
|
||||
db_member = db_result.scalar_one_or_none()
|
||||
assert db_member is None
|
||||
|
||||
async def test_delete_member_with_nonexistent_id(self, patched_session_maker):
|
||||
"""Test deleting a non-existent member raises MemberNotFoundError."""
|
||||
mutation = Mutation()
|
||||
|
||||
with pytest.raises(MemberNotFoundError, match="Member with ID 999 not found"):
|
||||
await mutation.delete_member(id=999)
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Integration tests for GraphQL query resolvers."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.models.member import Member
|
||||
from src.resolvers.member import Query
|
||||
|
||||
|
||||
class TestMemberQueries:
|
||||
"""Tests for GraphQL query resolvers."""
|
||||
|
||||
async def test_query_single_member_by_id(self, async_session, patched_session_maker):
|
||||
"""Test querying a single member by ID."""
|
||||
# Create a member
|
||||
member = Member(
|
||||
first_name="Alice",
|
||||
last_name="Smith",
|
||||
email="alice@example.com"
|
||||
)
|
||||
async_session.add(member)
|
||||
await async_session.commit()
|
||||
await async_session.refresh(member)
|
||||
|
||||
# Query the member
|
||||
query = Query()
|
||||
result = await query.member(id=member.id)
|
||||
|
||||
assert result is not None
|
||||
assert result.id == member.id
|
||||
assert result.first_name == "Alice"
|
||||
assert result.last_name == "Smith"
|
||||
assert result.email == "alice@example.com"
|
||||
|
||||
async def test_query_member_with_nonexistent_id(self, patched_session_maker):
|
||||
"""Test querying a member with non-existent ID returns None."""
|
||||
query = Query()
|
||||
result = await query.member(id=999)
|
||||
|
||||
assert result is None
|
||||
|
||||
async def test_query_all_members(self, async_session, patched_session_maker):
|
||||
"""Test querying all members."""
|
||||
# Create multiple members
|
||||
members = [
|
||||
Member(first_name="Alice", last_name="Smith"),
|
||||
Member(first_name="Bob", last_name="Johnson"),
|
||||
Member(first_name="Charlie", last_name="Brown"),
|
||||
]
|
||||
for m in members:
|
||||
async_session.add(m)
|
||||
await async_session.commit()
|
||||
|
||||
# Query all members
|
||||
query = Query()
|
||||
results = await query.members()
|
||||
|
||||
assert len(results) == 3
|
||||
assert results[0].first_name == "Charlie" # Brown comes first
|
||||
assert results[1].first_name == "Bob" # Johnson
|
||||
assert results[2].first_name == "Alice" # Smith
|
||||
|
||||
async def test_query_members_empty_database(self, patched_session_maker):
|
||||
"""Test querying members when database is empty returns empty array."""
|
||||
query = Query()
|
||||
results = await query.members()
|
||||
|
||||
assert results == []
|
||||
|
||||
async def test_members_sorting_nulls_last(self, async_session, patched_session_maker):
|
||||
"""Test that members query sorts by last_name (nulls last), then first_name."""
|
||||
# Create members with some having null last names
|
||||
members = [
|
||||
Member(first_name="Zoe", last_name="Anderson"),
|
||||
Member(first_name="Alice", last_name=None),
|
||||
Member(first_name="Bob", last_name=None),
|
||||
Member(first_name="Charlie", last_name="Baker"),
|
||||
]
|
||||
for m in members:
|
||||
async_session.add(m)
|
||||
await async_session.commit()
|
||||
|
||||
# Query all members
|
||||
query = Query()
|
||||
results = await query.members()
|
||||
|
||||
# Expected order: Anderson, Baker, then nulls (Alice, Bob)
|
||||
assert len(results) == 4
|
||||
assert results[0].last_name == "Anderson"
|
||||
assert results[1].last_name == "Baker"
|
||||
assert results[2].first_name == "Alice"
|
||||
assert results[2].last_name is None
|
||||
assert results[3].first_name == "Bob"
|
||||
assert results[3].last_name is None
|
||||
Reference in New Issue
Block a user