# project-setup Specification ## Purpose TBD - created by archiving change add-graphql-member-api. Update Purpose after archive. ## Requirements ### Requirement: Python project structure with uv dependency management The project MUST use Python 3.11+ with uv for fast, reliable dependency management and provide a standard src/ layout for code organization. #### Scenario: Developer initializes new development environment **Given** a developer has cloned the repository **When** they run `uv sync` **Then** all dependencies are installed in a virtual environment **And** the environment is ready for development within 30 seconds #### Scenario: Developer runs the application **Given** dependencies are installed **When** developer runs `uv run uvicorn src.main:app --reload` **Then** the FastAPI server starts on http://localhost:8000 **And** GraphQL playground is available at http://localhost:8000/graphql ### Requirement: Project configuration in pyproject.toml The project MUST define all metadata, dependencies, and tool configurations in pyproject.toml following modern Python packaging standards. **Dependencies required:** - fastapi >= 0.104.0 (Web framework) - strawberry-graphql[fastapi] >= 0.215.0 (GraphQL integration) - sqlalchemy[asyncio] >= 2.0.0 (ORM with async support) - aiosqlite >= 0.19.0 (Async SQLite driver) - alembic >= 1.12.0 (Database migrations) - pydantic-settings >= 2.0.0 (Configuration management) - uvicorn[standard] >= 0.24.0 (ASGI server) **Development dependencies required:** - pytest >= 7.4.0 (Test framework) - pytest-asyncio >= 0.21.0 (Async test support) - black >= 23.0.0 (Code formatting) - ruff >= 0.1.0 (Linting) - isort >= 5.12.0 (Import sorting) #### Scenario: Dependencies are declared with version constraints **Given** pyproject.toml exists **When** developer inspects [project.dependencies] **Then** all required packages are listed with minimum versions **And** version constraints allow patch/minor updates #### Scenario: Development tools are configured **Given** pyproject.toml contains tool configurations **When** developer runs `black .` **Then** code is formatted with line length 88 **When** developer runs `ruff check .` **Then** code is linted against configured rules ### Requirement: Source directory structure following layered architecture The project MUST organize code into src/ directory with clear separation of concerns across API, business logic, and data layers. **Required directory structure:** ``` src/ ├── __init__.py ├── main.py # FastAPI application, startup/shutdown ├── config.py # Settings and configuration ├── database.py # Database connection, session factory ├── models/ # SQLAlchemy ORM models │ ├── __init__.py │ └── member.py ├── schemas/ # Strawberry GraphQL types │ ├── __init__.py │ └── member.py └── resolvers/ # GraphQL query/mutation resolvers ├── __init__.py └── member.py ``` #### Scenario: Code is organized by architectural layer **Given** the src/ directory exists **When** a developer navigates the codebase **Then** models/ contains only SQLAlchemy ORM definitions **And** schemas/ contains only Strawberry GraphQL type definitions **And** resolvers/ contains only GraphQL resolver functions **And** each module has clear, single responsibility #### Scenario: Main application entry point is defined **Given** src/main.py exists **When** the file is imported **Then** it exports a FastAPI `app` instance **And** app includes GraphQL route at /graphql **And** app includes startup event to verify database connection ### Requirement: Testing infrastructure with pytest The project MUST provide pytest configuration for running async tests with database fixtures and code coverage reporting. #### Scenario: Async tests can be executed **Given** pytest and pytest-asyncio are installed **When** developer runs `pytest` **Then** all tests in tests/ directory are discovered **And** async test functions execute correctly **And** test results are displayed with pass/fail status #### Scenario: Database fixtures are available for tests **Given** tests/conftest.py defines database fixtures **When** a test function requests `db_session` fixture **Then** an isolated test database session is provided **And** session is rolled back after test completion **And** no test data persists between test runs ### Requirement: Code quality tooling configuration The project MUST configure black, ruff, and isort for consistent code formatting and linting with settings in pyproject.toml. **Black configuration:** - Line length: 88 characters - Target version: Python 3.11 - Skip string normalization: false **Ruff configuration:** - Line length: 88 characters - Select: E, F, W, I (pycodestyle, pyflakes, warnings, isort) - Ignore: E501 (line too long, handled by black) **Isort configuration:** - Profile: black (compatible settings) - Multi-line output: 3 (vertical hanging indent) #### Scenario: Code formatting is enforced **Given** black is configured in pyproject.toml **When** developer runs `black --check .` **Then** all Python files are checked for formatting **And** exit code is 0 if all files are formatted correctly **And** exit code is 1 if any files need formatting #### Scenario: Code quality checks pass **Given** ruff is configured in pyproject.toml **When** developer runs `ruff check .` **Then** all Python files are linted **And** no errors are reported for compliant code **And** clear error messages are shown for violations ### Requirement: Environment configuration with .env support The project MUST support environment-based configuration using .env files with pydantic-settings for type-safe config values. #### Scenario: Default configuration works for development **Given** no .env file exists **When** application starts **Then** it uses default SQLite database path **And** it runs in debug mode **And** application starts successfully #### Scenario: Environment variables override defaults **Given** .env file contains `DATABASE_URL=sqlite+aiosqlite:///./test.db` **When** application loads configuration **Then** settings.database_url equals "sqlite+aiosqlite:///./test.db" **And** custom database path is used #### Scenario: Configuration is type-safe **Given** src/config.py defines Settings class **When** invalid configuration value is provided **Then** pydantic validation raises clear error **And** application fails fast on startup ### Requirement: Development scripts for common tasks The project MUST provide executable scripts for database seeding and common development tasks. **Required scripts:** - scripts/seed.py - Populate database with sample member #### Scenario: Seed script creates sample data **Given** database schema exists from migrations **When** developer runs `uv run python scripts/seed.py` **Then** sample member is created in database **And** script is idempotent (safe to run multiple times) **And** success message is displayed ### Requirement: Automated Test Suite with Pytest The project SHALL provide a comprehensive test suite using pytest to verify all features and prevent regressions. #### Scenario: Run test suite successfully - **GIVEN** the project is set up with dependencies installed - **WHEN** the developer runs `uv run pytest` - **THEN** all tests SHALL execute - **AND** test results SHALL be displayed with pass/fail status - **AND** the command SHALL exit with code 0 if all tests pass #### Scenario: Run tests with verbose output - **GIVEN** the project has tests implemented - **WHEN** the developer runs `uv run pytest --verbose` - **THEN** detailed test output SHALL be displayed - **AND** each test name and result SHALL be shown #### Scenario: Run specific test categories - **GIVEN** tests are organized in subdirectories - **WHEN** the developer runs `uv run pytest tests/unit` - **THEN** only unit tests SHALL execute - **WHEN** the developer runs `uv run pytest tests/integration` - **THEN** only integration tests SHALL execute ### Requirement: Test Organization Structure Tests SHALL be organized in a clear directory structure separating different test types. #### Scenario: Test directory structure exists - **GIVEN** the project repository - **WHEN** inspecting the directory structure - **THEN** a `tests/` directory SHALL exist at the project root - **AND** subdirectories `tests/unit/`, `tests/integration/`, `tests/e2e/`, and `tests/mcp/` SHALL exist - **AND** a `tests/conftest.py` file SHALL exist for shared fixtures #### Scenario: Test files follow naming convention - **GIVEN** test files in the `tests/` directory - **WHEN** examining file names - **THEN** all test files SHALL be named `test_*.py` - **AND** test file names SHALL correspond to the modules they test (e.g., `test_validation.py` for `validation.py`) ### Requirement: Shared Test Fixtures The test suite SHALL provide reusable fixtures for common test setup via conftest.py. #### Scenario: Database fixture provides in-memory SQLite - **GIVEN** tests need database access - **WHEN** a test function requests the `db` fixture - **THEN** an in-memory SQLite database SHALL be created - **AND** the database SHALL have all tables created via Alembic migrations or direct schema creation - **AND** the database SHALL be cleaned up after the test completes #### Scenario: Async session fixture provides database sessions - **GIVEN** tests need to interact with the database - **WHEN** a test function requests the `async_session` fixture - **THEN** an async SQLAlchemy session SHALL be provided - **AND** the session SHALL be connected to the test database - **AND** the session SHALL be closed after the test completes #### Scenario: GraphQL client fixture provides test client - **GIVEN** tests need to make GraphQL requests - **WHEN** a test function requests the `graphql_client` fixture - **THEN** a FastAPI test client SHALL be provided - **AND** the client SHALL be configured to use the test database - **AND** the client SHALL support async GraphQL queries and mutations ### Requirement: Unit Tests for Validation Logic Unit tests SHALL verify validation functions work correctly with valid and invalid inputs. #### Scenario: Test firstName validation with valid input - **GIVEN** a valid firstName "Alice" - **WHEN** `validate_first_name("Alice")` is called - **THEN** no exception SHALL be raised #### Scenario: Test firstName validation with empty string - **GIVEN** an empty firstName "" - **WHEN** `validate_first_name("")` is called - **THEN** a ValidationError SHALL be raised - **AND** the error message SHALL indicate firstName cannot be empty #### Scenario: Test email validation with valid email - **GIVEN** a valid email "test@example.com" - **WHEN** `validate_email("test@example.com")` is called - **THEN** no exception SHALL be raised #### Scenario: Test email validation with invalid email - **GIVEN** an invalid email "not-an-email" - **WHEN** `validate_email("not-an-email")` is called - **THEN** a ValidationError SHALL be raised - **AND** the error message SHALL indicate invalid email format #### Scenario: Test email validation with None - **GIVEN** email is None - **WHEN** `validate_email(None)` is called - **THEN** no exception SHALL be raised (None is allowed) #### Scenario: Test phone validation with valid E.164 number - **GIVEN** a valid phone "+41791234567" - **WHEN** `validate_phone("+41791234567")` is called - **THEN** no exception SHALL be raised #### Scenario: Test phone validation with invalid number - **GIVEN** an invalid phone "123" - **WHEN** `validate_phone("123")` is called - **THEN** a ValidationError SHALL be raised #### Scenario: Test phone validation with None - **GIVEN** phone is None - **WHEN** `validate_phone(None)` is called - **THEN** no exception SHALL be raised (None is allowed) ### Requirement: Integration Tests for Member Model Integration tests SHALL verify database operations with the Member model. #### Scenario: Create member with minimal data - **GIVEN** a test database session - **WHEN** a Member is created with only firstName="Alice" - **THEN** the member SHALL be persisted to the database - **AND** the member SHALL have an auto-generated ID - **AND** created_at and updated_at timestamps SHALL be populated - **AND** all optional fields SHALL be None #### Scenario: Create member with complete data - **GIVEN** a test database session - **WHEN** a Member is created with all fields populated - **THEN** the member SHALL be persisted with all data - **AND** all fields SHALL match the provided values #### Scenario: Member timestamps are set correctly - **GIVEN** a newly created member - **THEN** created_at SHALL be set to current time - **AND** updated_at SHALL be set to current time - **AND** created_at SHALL equal updated_at for new members ### Requirement: Integration Tests for GraphQL Queries Integration tests SHALL verify GraphQL query resolvers return correct data. #### Scenario: Query single member by ID - **GIVEN** a member exists with ID 1 - **WHEN** the `member(id: 1)` query is executed - **THEN** the member data SHALL be returned - **AND** all requested fields SHALL match the database record #### Scenario: Query member with non-existent ID - **GIVEN** no member exists with ID 999 - **WHEN** the `member(id: 999)` query is executed - **THEN** None SHALL be returned - **AND** no error SHALL be raised #### Scenario: List all members - **GIVEN** multiple members exist in the database - **WHEN** the `members` query is executed - **THEN** an array of all members SHALL be returned - **AND** members SHALL be sorted by last_name (nulls last), then first_name #### Scenario: List members when database is empty - **GIVEN** no members exist in the database - **WHEN** the `members` query is executed - **THEN** an empty array SHALL be returned ### Requirement: Integration Tests for GraphQL Mutations Integration tests SHALL verify GraphQL mutation resolvers create, update, and delete members correctly. #### Scenario: Create member with minimal data - **GIVEN** CreateMemberInput with only firstName="Bob" - **WHEN** the `createMember` mutation is executed - **THEN** a new member SHALL be created in the database - **AND** the member SHALL have firstName="Bob" - **AND** all optional fields SHALL be None - **AND** the mutation SHALL return the created member with ID #### Scenario: Create member with complete data - **GIVEN** CreateMemberInput with all fields populated - **WHEN** the `createMember` mutation is executed - **THEN** a new member SHALL be created with all data - **AND** all fields SHALL match the input #### Scenario: Create member with invalid email - **GIVEN** CreateMemberInput with email="not-an-email" - **WHEN** the `createMember` mutation is executed - **THEN** a ValidationError SHALL be raised - **AND** no database record SHALL be created #### Scenario: Update member successfully - **GIVEN** a member exists with ID 1 - **WHEN** the `updateMember` mutation is executed with id=1 and email="new@example.com" - **THEN** the member's email SHALL be updated to "new@example.com" - **AND** the updated_at timestamp SHALL be refreshed - **AND** all other fields SHALL remain unchanged #### Scenario: Update member with non-existent ID - **GIVEN** no member exists with ID 999 - **WHEN** the `updateMember` mutation is executed with id=999 - **THEN** a MemberNotFoundError SHALL be raised - **AND** the error message SHALL indicate member not found #### Scenario: Delete member successfully - **GIVEN** a member exists with ID 1 - **WHEN** the `deleteMember` mutation is executed with id=1 - **THEN** the member SHALL be removed from the database - **AND** the mutation SHALL return true - **AND** subsequent queries for that member SHALL return None #### Scenario: Delete member with non-existent ID - **GIVEN** no member exists with ID 999 - **WHEN** the `deleteMember` mutation is executed with id=999 - **THEN** a MemberNotFoundError SHALL be raised ### Requirement: E2E Tests for GraphQL API End-to-end tests SHALL verify complete GraphQL request/response flows over HTTP. #### Scenario: Execute GraphQL query via HTTP POST - **GIVEN** the FastAPI application with GraphQL endpoint - **WHEN** a POST request is sent to /graphql with a query - **THEN** the query SHALL be executed - **AND** a JSON response SHALL be returned with data or errors #### Scenario: GraphQL introspection query - **GIVEN** the GraphQL API is running - **WHEN** an introspection query for __schema is executed - **THEN** the complete schema SHALL be returned - **AND** all types, queries, and mutations SHALL be included #### Scenario: GraphQL type introspection - **GIVEN** the GraphQL API is running - **WHEN** an introspection query for __type(name: "Member") is executed - **THEN** the Member type definition SHALL be returned - **AND** all fields with their types SHALL be included ### Requirement: MCP Server Tests Tests SHALL verify MCP server tool definitions and execution. #### Scenario: MCP server initializes successfully - **GIVEN** a valid CLUBBER_API_URL - **WHEN** the MCP server is instantiated - **THEN** the server SHALL initialize without errors - **AND** the GraphQL client SHALL be configured #### Scenario: List tools returns all 6 tools - **GIVEN** an initialized MCP server - **WHEN** `list_tools()` is called - **THEN** 6 tools SHALL be returned - **AND** tools SHALL include: list_members, get_member, create_member, update_member, get_graphql_schema, execute_graphql_query #### Scenario: Execute list_members tool - **GIVEN** members exist in the database - **WHEN** the `list_members` tool is invoked - **THEN** a formatted list of all members SHALL be returned #### Scenario: Execute get_member tool - **GIVEN** a member exists with ID 1 - **WHEN** the `get_member` tool is invoked with id=1 - **THEN** the member's data SHALL be returned as formatted text #### Scenario: Execute create_member tool - **GIVEN** valid member input data - **WHEN** the `create_member` tool is invoked - **THEN** a new member SHALL be created via the GraphQL API - **AND** the created member data SHALL be returned #### Scenario: Execute get_graphql_schema tool - **GIVEN** the GraphQL API is accessible - **WHEN** the `get_graphql_schema` tool is invoked - **THEN** the complete schema SHALL be retrieved via introspection - **AND** formatted schema information SHALL be returned #### Scenario: Execute execute_graphql_query tool with simple query - **GIVEN** a valid GraphQL query string - **WHEN** the `execute_graphql_query` tool is invoked - **THEN** the query SHALL be executed against the API - **AND** formatted JSON results SHALL be returned #### Scenario: Execute execute_graphql_query tool with variables - **GIVEN** a GraphQL query with variables - **WHEN** the `execute_graphql_query` tool is invoked with query and variables - **THEN** the query SHALL be executed with variable substitution - **AND** results SHALL be returned ### Requirement: Test Documentation in README The README SHALL document how to run tests and understand test organization. #### Scenario: README contains test running instructions - **GIVEN** the README.md file - **WHEN** reading the documentation - **THEN** a "Running Tests" section SHALL exist - **AND** the section SHALL document the `uv run pytest` command - **AND** examples of running specific test categories SHALL be provided #### Scenario: README documents test organization - **GIVEN** the README.md file - **WHEN** reading the documentation - **THEN** a "Test Organization" section SHALL exist - **AND** the section SHALL explain the tests/ directory structure - **AND** the section SHALL describe unit, integration, e2e, and mcp test categories