# Story 1.2: Test Infrastructure — conftest.py

Status: done

## Story

As a developer,
I want `tests/conftest.py` with an in-memory SQLite test fixture,
so that all database-touching tests run in complete isolation from the real database, enabling fast and repeatable tests from the first story onwards.

## Acceptance Criteria

1. **Given** the scaffold from Story 1.1 exists, **when** `tests/conftest.py` is created, **then** it defines an `app` pytest fixture that creates the Flask app with `TESTING=True` and `SQLALCHEMY_DATABASE_URI='sqlite:///:memory:'`.
2. It defines a `db` pytest fixture that calls `db.create_all()` before yielding and `db.drop_all()` after, using `app.app_context()`.
3. It defines a `client` pytest fixture wrapping the test app with Flask's test client.
4. `pytest tests/` runs with 0 failures and 0 errors (0 or exit-code-5 "no tests collected" is acceptable).

## Tasks / Subtasks

- [x] **Task 1: Implement minimal `app/extensions.py`** (AC: 1, 2)
  - [x] Replace the stub comments with real code: `db = SQLAlchemy()` and `migrate = Migrate()`
  - [x] This is the minimum needed for conftest.py to import `db`
  - [x] **Do NOT add WAL mode here** — that is Story 1.5 via `@event.listens_for`

- [x] **Task 2: Implement minimal `app/__init__.py`** (AC: 1)
  - [x] Replace the stub comments with a real `create_app(config=None)` function
  - [x] Factory must: set default config values, call `db.init_app(app)` and `migrate.init_app(app, db)`, accept an override dict via the `config` parameter, return the `app`
  - [x] **Do NOT register any blueprints yet** — that is Story 1.5
  - [x] **Do NOT add WAL mode yet** — that is Story 1.5
  - [x] **Do NOT add error handlers yet** — that is Story 2.1
  - [x] See Dev Notes for exact implementation

- [x] **Task 3: Create `tests/conftest.py`** (AC: 1, 2, 3)
  - [x] Create `tests/conftest.py` with exactly three fixtures: `app`, `db`, `client`
  - [x] `app` fixture: calls `create_app(...)` with test overrides, pushes app context via `with app.app_context()`, yields the app
  - [x] `db` fixture: depends on `app` fixture, calls `_db.create_all()`, yields `_db`, calls `_db.drop_all()`
  - [x] `client` fixture: depends on `app` fixture, returns `app.test_client()`
  - [x] See Dev Notes for exact implementation

- [x] **Task 4: Verify pytest runs clean** (AC: 4)
  - [x] Run: `source .venv/bin/activate && python -m pytest tests/ -v`
  - [x] Confirm: no `ImportError`, no `AttributeError`, no `SyntaxError` in output
  - [x] Confirm: 0 failures, 0 errors (exit 0 or exit 5 — both acceptable)

## Dev Notes

### ⚠️ The Import Chain Problem (Must Read)

`tests/conftest.py` will import `create_app` from `app` and `db` from `app.extensions`. Both files are currently **empty comment stubs** from Story 1.1. If conftest.py runs against empty stubs, pytest fails with `ImportError` — which violates AC-4.

**This story must implement a minimal but real `app/extensions.py` and `app/__init__.py`** so the import chain succeeds. These are intentionally incomplete implementations — Story 1.5 completes them.

### What Story 1.2 Implements vs. What Story 1.5 Completes

| Concern | Story 1.2 (minimum) | Story 1.5 (completes) |
|---------|--------------------|-----------------------|
| `db = SQLAlchemy()` | ✅ | — |
| `migrate = Migrate()` | ✅ | — |
| `create_app(config)` function | ✅ minimal | ✅ full |
| `db.init_app(app)` | ✅ | — |
| `migrate.init_app(app, db)` | ✅ | — |
| WAL mode event listener | ❌ | ✅ |
| Blueprint registration | ❌ | ✅ |
| Config classes from config.py | ❌ | ✅ |
| Error handlers (404/500) | ❌ | ✅ |
| `flask run` working | ❌ | ✅ |
| `wsgi.py` complete | ❌ | ✅ |
| `date_utils.py` with tests | ❌ | ✅ |
| `validators.py` with tests | ❌ | ✅ |

### Exact Implementation: `app/extensions.py`

Replace the existing stub with:

```python
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()
```

That's the complete file for Story 1.2. Story 1.5 adds the WAL mode event listener here.

### Exact Implementation: `app/__init__.py`

Replace the existing stub with:

```python
from flask import Flask
from .extensions import db, migrate


def create_app(config=None):
    """Minimal application factory — Story 1.5 adds blueprints, WAL mode, error handlers."""
    app = Flask(__name__)

    # Default configuration
    app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///financials.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['WTF_CSRF_ENABLED'] = True

    # Test / override config (dict passed directly)
    if config:
        app.config.update(config)

    # Initialize extensions
    db.init_app(app)
    migrate.init_app(app, db)

    return app
```

**Do not add blueprint registration or WAL mode here — Story 1.5 owns those.**

### Exact Implementation: `tests/conftest.py`

```python
import pytest
from app import create_app
from app.extensions import db as _db


@pytest.fixture()
def app():
    """Create application with in-memory SQLite for testing."""
    app = create_app({
        'TESTING': True,
        'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
        'WTF_CSRF_ENABLED': False,
        'SECRET_KEY': 'test-secret-key',
    })
    with app.app_context():
        yield app


@pytest.fixture()
def db(app):
    """Create all tables before each test, drop after."""
    _db.create_all()
    yield _db
    _db.drop_all()


@pytest.fixture()
def client(app):
    """Flask test client."""
    return app.test_client()
```

### Design Decisions

**Why `with app.app_context()` in the `app` fixture, not the `db` fixture?**

Pushing the app context in `app` (not `db`) means any fixture that depends on `app` — including `client` — automatically runs within an active application context. This avoids `RuntimeError: Working outside of application context` when Flask-SQLAlchemy operations occur inside test functions that only request `client`.

**Why `_db` alias for the import?**

The fixture is named `db` to match the AC and be ergonomic for tests (`def test_foo(db, client)`). Aliasing the import as `_db` avoids name collision.

**Why `WTF_CSRF_ENABLED: False` in test config?**

CSRF tokens would require all test POST requests to include a valid token, making form tests cumbersome. Disabled only in test environment.

**Why does `db.create_all()` succeed with no models yet?**

`db.create_all()` creates tables for all models currently registered with SQLAlchemy's metadata. In Story 1.2, no models are registered (they're all stubs), so it creates 0 tables — this is completely valid and raises no error.

### Flask-SQLAlchemy 3.x Specifics

This project uses **Flask-SQLAlchemy 3.1.1**. Key differences from 2.x:
- `db.create_all()` / `db.drop_all()` require an active app context (handled by pushing context in the `app` fixture)
- `db.session` is scoped to the request/test context — no global session leak between tests
- No `SQLALCHEMY_POOL_SIZE` warning for SQLite (in-memory SQLite doesn't pool)

### Previous Story Learnings (Story 1.1)

- Python 3.12.3, pytest 9.0.3, Flask-SQLAlchemy 3.1.1, Flask-Migrate 4.1.0 in `.venv/`
- Exit code 5 ("no tests collected") is the expected result — same as Story 1.1
- `pytest.ini` already configured with `testpaths = tests` — no changes needed
- `app/__init__.py` and `app/extensions.py` are currently empty comment stubs — this story replaces them with minimal real code

### Files Modified in This Story

| File | Status | Notes |
|------|--------|-------|
| `tests/conftest.py` | **NEW** | First real code file in the project |
| `app/extensions.py` | **MODIFY** | Replace stub with real `db` and `migrate` objects |
| `app/__init__.py` | **MODIFY** | Replace stub with minimal `create_app()` |

### Architecture Constraints

- **AR-1:** This is the first real implementation file. No model code exists yet — `db.create_all()` creates 0 tables (correct).
- **AR-2:** `test_duplicate_detector.py` comes in Story 1.3 — NOT this story.
- **Boundary 1 (Flask/Service):** `create_app()` must not import any service modules. It stays minimal.
- **Boundary 2 (Service/DB):** No service code is written yet — conftest.py only wires the Flask app and db.

### References

- [Source: docs/epics.md#Story-1.2] — Acceptance criteria and ordering note
- [Source: docs/architecture.md#Starter-Template] — `create_app()` factory pattern
- [Source: docs/architecture.md#Technical-Constraints] — Flask-SQLAlchemy 3.x, in-memory SQLite for tests
- [Source: docs/stories/1-1-project-scaffold-environment-setup.md] — Scaffold context and previous learnings

## Dev Agent Record

### Agent Model Used

claude-sonnet-4-6

### Debug Log References

None — clean implementation.

### Completion Notes List

- Replaced `app/extensions.py` stub with `db = SQLAlchemy()` and `migrate = Migrate()`. WAL mode intentionally deferred to Story 1.5.
- Replaced `app/__init__.py` stub with minimal `create_app(config=None)` — initializes db and migrate, accepts a config dict override. No blueprints, no WAL mode, no error handlers — all Story 1.5.
- Created `tests/conftest.py` with three fixtures: `app` (pushes app context via `with app.app_context()`), `db` (create_all/drop_all around each test), `client` (test_client()).
- `db.create_all()` with no models registered creates 0 tables — correct and expected at this stage.
- Smoke-tested all three fixture operations outside pytest — all passed: `db.create_all() OK`, `db.drop_all() OK`, `FlaskClient OK`.
- `pytest tests/ -v` → exit 5, 0 collected, 0 failures, 0 errors ✓.

### Change Log

- 2026-05-27: Story 1.2 implemented — `tests/conftest.py` created; `app/extensions.py` and `app/__init__.py` upgraded from stubs to minimal working implementations. (claude-sonnet-4-6)

### File List

- `tests/conftest.py` — **NEW**: three pytest fixtures (app, db, client)
- `app/extensions.py` — **MODIFIED**: replaced stub with `db = SQLAlchemy()`, `migrate = Migrate()`
- `app/__init__.py` — **MODIFIED**: replaced stub with minimal `create_app(config=None)` factory
