[t-020] Create Full pytest test suite (conftest + 5 test files) #20

Open
opened 2026-07-05 08:31:02 +00:00 by david · 0 comments
Owner

Goal

Write comprehensive tests for all routes, services, and models.

References

Reference

  • AGENT.md §12 Tests (pytest + pytest-asyncio patterns)
  • DESIGN.md §4 Build Phases (test coverage requirements)

Implementation Steps

Implementation Steps

1. Create tests/conftest.py

# tests/conftest.py
import os
import tempfile
from pathlib import Path
import pytest
from src.server import app
from fastapi.testclient import TestClient

@pytest.fixture
def client():
    return TestClient(app)

@pytest.fixture
def auth_headers():
    return {"Authorization": "Bearer keryx-bff-change-me"}

@pytest.fixture(autouse=True)
def setup_test_config():
    """Use a temporary config for tests."""
    with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
        f.write('''app: {name: Keryx, version: 0.1.0, supported_app_versions: ^1.0.0}
server: {host: 0.0.0.0, port: 8643, api_key: keryx-bff-change-me}
hermes: {api_url: http://127.0.0.1:8642, api_key: hermes-key, workspace_path: /tmp/test-workspace}
bff: {staging_dir: /tmp/keryx-test, db_path: /tmp/keryx.db, max_upload_size_mb: 50, staging_cleanup_days: 30}''')
        config_path = f.name
    os.environ['KERYX_CONFIG'] = config_path
    yield
    os.unlink(config_path)

2. Create tests/test_app_data.py

# tests/test_app_data.py
def test_app_data_endpoint(client, auth_headers):
    resp = client.get('/v1/ios/app_data', headers=auth_headers)
    assert resp.status_code == 200
    data = resp.json()
    assert data['app']['name'] == 'Keryx'
    assert data['navigation']['primaryTab']['label'] == 'Chats'

def test_app_data_no_auth(client):
    resp = client.get('/v1/ios/app_data')
    assert resp.status_code == 401

3. Create tests/test_chat_stream.py

# tests/test_chat_stream.py
def test_chat_stream_route_exists(client, auth_headers):
    # Verify route exists (Hermes may not be running so response will error)
    resp = client.post('/v1/ios/chat/s1/stream', headers=auth_headers, json={'message': 'test'})
    assert resp.status_code != 404  # route must exist

4. Create tests/test_attach.py

# tests/test_attach.py
def test_attach_file(client, auth_headers):
    from io import BytesIO
    resp = client.post(
        '/v1/ios/chat/s1/attach',
        headers=auth_headers,
        files={'file': ('test.txt', BytesIO(b'hello world'), 'text/plain')},
    )
    assert resp.status_code == 200
    data = resp.json()
    assert data['name'] == 'test.txt'

def test_attach_unsupported_mime(client, auth_headers):
    from io import BytesIO
    resp = client.post(
        '/v1/ios/chat/s1/attach',
        headers=auth_headers,
        files={'file': ('unknown.bin', BytesIO(b'x'), 'application/octet-stream')},
    )
    assert resp.status_code == 415

5. Create tests/test_workspace.py

# tests/test_workspace.py
def test_workspace_path_traversal_blocked(client, auth_headers):
    resp = client.get('/v1/ios/workspace/read/../../../etc/passwd', headers=auth_headers)
    assert resp.status_code != 200

6. Create tests/test_graphql.py

# tests/test_graphql.py
def test_graphql_app_data_query(client, auth_headers):
    resp = client.post(
        '/graphql',
        headers=auth_headers,
        json={'query': '{ appData { app { name } navigation { primaryTab { label } } } }'},
    )
    assert resp.status_code == 200
    data = resp.json()['data']['appData']
    assert data['app']['name'] == 'Keryx'

Verification Gate

Verification Gate

Run:

cd /home/david/Projects/keryx-bff && uv run pytest -v tests/
# Expected: all tests pass, 0 failures (some may skip if Hermes not running — that's acceptable)

STOP Conditions

STOP Conditions

  • If any test fails with status code != expected, stop and fix the failing route or model.

Depends on: t-018

## Goal Write comprehensive tests for all routes, services, and models. ## References ## Reference - AGENT.md §12 Tests (pytest + pytest-asyncio patterns) - DESIGN.md §4 Build Phases (test coverage requirements) ## Implementation Steps ## Implementation Steps ### 1. Create `tests/conftest.py` ```python # tests/conftest.py import os import tempfile from pathlib import Path import pytest from src.server import app from fastapi.testclient import TestClient @pytest.fixture def client(): return TestClient(app) @pytest.fixture def auth_headers(): return {"Authorization": "Bearer keryx-bff-change-me"} @pytest.fixture(autouse=True) def setup_test_config(): """Use a temporary config for tests.""" with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: f.write('''app: {name: Keryx, version: 0.1.0, supported_app_versions: ^1.0.0} server: {host: 0.0.0.0, port: 8643, api_key: keryx-bff-change-me} hermes: {api_url: http://127.0.0.1:8642, api_key: hermes-key, workspace_path: /tmp/test-workspace} bff: {staging_dir: /tmp/keryx-test, db_path: /tmp/keryx.db, max_upload_size_mb: 50, staging_cleanup_days: 30}''') config_path = f.name os.environ['KERYX_CONFIG'] = config_path yield os.unlink(config_path) ``` ### 2. Create `tests/test_app_data.py` ```python # tests/test_app_data.py def test_app_data_endpoint(client, auth_headers): resp = client.get('/v1/ios/app_data', headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert data['app']['name'] == 'Keryx' assert data['navigation']['primaryTab']['label'] == 'Chats' def test_app_data_no_auth(client): resp = client.get('/v1/ios/app_data') assert resp.status_code == 401 ``` ### 3. Create `tests/test_chat_stream.py` ```python # tests/test_chat_stream.py def test_chat_stream_route_exists(client, auth_headers): # Verify route exists (Hermes may not be running so response will error) resp = client.post('/v1/ios/chat/s1/stream', headers=auth_headers, json={'message': 'test'}) assert resp.status_code != 404 # route must exist ``` ### 4. Create `tests/test_attach.py` ```python # tests/test_attach.py def test_attach_file(client, auth_headers): from io import BytesIO resp = client.post( '/v1/ios/chat/s1/attach', headers=auth_headers, files={'file': ('test.txt', BytesIO(b'hello world'), 'text/plain')}, ) assert resp.status_code == 200 data = resp.json() assert data['name'] == 'test.txt' def test_attach_unsupported_mime(client, auth_headers): from io import BytesIO resp = client.post( '/v1/ios/chat/s1/attach', headers=auth_headers, files={'file': ('unknown.bin', BytesIO(b'x'), 'application/octet-stream')}, ) assert resp.status_code == 415 ``` ### 5. Create `tests/test_workspace.py` ```python # tests/test_workspace.py def test_workspace_path_traversal_blocked(client, auth_headers): resp = client.get('/v1/ios/workspace/read/../../../etc/passwd', headers=auth_headers) assert resp.status_code != 200 ``` ### 6. Create `tests/test_graphql.py` ```python # tests/test_graphql.py def test_graphql_app_data_query(client, auth_headers): resp = client.post( '/graphql', headers=auth_headers, json={'query': '{ appData { app { name } navigation { primaryTab { label } } } }'}, ) assert resp.status_code == 200 data = resp.json()['data']['appData'] assert data['app']['name'] == 'Keryx' ``` ## Verification Gate ## Verification Gate Run: ```bash cd /home/david/Projects/keryx-bff && uv run pytest -v tests/ # Expected: all tests pass, 0 failures (some may skip if Hermes not running — that's acceptable) ``` ## STOP Conditions ## STOP Conditions - If any test fails with status code != expected, stop and fix the failing route or model. Depends on: t-018
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: david/keryx-bff#20
No description provided.