Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ __pycache__/
*.pyo
*.pyd
.Python
env/
venv/
ENV/
env.bak/
venv.bak/
.vscode/
.pytest_cache/
instance/
*.db
*.sqlite
*.sqlite3
*.so
*.egg
*.egg-info/
Expand All @@ -12,4 +23,9 @@ build/
instance/*.db
.env
.DS_Store
*.log
*.log
.pytest_cache/
.coverage
htmlcov/
.tox/
.cache/
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@ Set a coding goal, set a deadline, and watch the countdown tick. The timer only
4. **Open locally in your browser**:
Navigate to `http://localhost:5000`

## Testing

The project includes automated tests using pytest to ensure code quality and prevent regressions.

### Running Tests

1. **Install test dependencies** (included in requirements.txt):
```bash
pip install -r requirements.txt
```

2. **Run the test suite**:
```bash
pytest
```

3. **Run tests with coverage**:
```bash
pytest --cov=application --cov-report=html
```

### Test Structure

- `tests/test_api.py`: API endpoint tests (health check, etc.)
- `tests/test_models.py`: Model unit tests (Goal.to_dict(), etc.)
- `tests/conftest.py`: Shared fixtures for Flask app and database setup

Tests use an in-memory SQLite database for isolation and speed.

## Usage

1. **Login with GitHub** - Authenticate via OAuth2
Expand Down
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ requests
python-dotenv
gunicorn
psycopg2-binary
pytest>=7.4.0
pytest-flask
pytest-cov
Empty file added tests/__init__.py
Empty file.
38 changes: 38 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest
import os

# Set test environment variables before importing application
os.environ['DATABASE_URL'] = 'sqlite:///:memory:'
os.environ['SECRET_KEY'] = 'test-secret-key'
os.environ['BASE_URL'] = 'http://localhost:5000'

from application import application, db

@pytest.fixture(scope='session')
def app():
"""Create and configure a test app instance."""
# App is already configured with test settings
test_app = application

# Create database tables
with test_app.app_context():
db.create_all()

yield test_app

@pytest.fixture(scope='session')
def client(app):
"""A test client for the app."""
return app.test_client()

@pytest.fixture(scope='function')
def session(app):
"""Create a new database session for a test."""
with app.app_context():
# Start a transaction
db.session.begin_nested()

yield db.session

# Rollback the transaction after the test
db.session.rollback()
29 changes: 29 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json
from datetime import datetime

def test_health_endpoint(client):
"""Test the /api/health endpoint returns healthy status."""
response = client.get('/api/health')

# Check status code
assert response.status_code == 200

# Parse JSON response
data = json.loads(response.data)

# Check required fields are present
assert 'status' in data
assert 'timestamp' in data
assert 'service' in data

# Check status is healthy
assert data['status'] == 'healthy'

# Check service name
assert data['service'] == 'git-done-api'

# Check timestamp is valid ISO format
try:
datetime.fromisoformat(data['timestamp'])
except ValueError:
pytest.fail("Timestamp is not in valid ISO format")
108 changes: 108 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import pytest
from datetime import datetime, timedelta, UTC
from application import Goal

def test_goal_to_dict_required_fields(session):
"""Test Goal.to_dict() with all required fields."""
# Create a test goal
deadline = datetime.now(UTC) + timedelta(days=7)
goal = Goal(
user_github_id='12345',
description='Test goal',
deadline=deadline,
repo_url='https://github.com/user/repo',
completion_condition='fix bug',
status='active',
repo_owner='user',
repo_name='repo',
embed_token='test-token'
)
session.add(goal)
session.commit()

result = goal.to_dict()

# Check all expected fields are present
expected_fields = [
'id', 'user_github_id', 'description', 'deadline', 'repo_url',
'completion_condition', 'status', 'created_at', 'embed_token', 'embed_url'
]

for field in expected_fields:
assert field in result, f"Missing field: {field}"

# Check data types
assert isinstance(result['id'], int)
assert isinstance(result['user_github_id'], str)
assert isinstance(result['description'], str)
assert isinstance(result['deadline'], str)
assert isinstance(result['repo_url'], str)
assert isinstance(result['completion_condition'], str)
assert isinstance(result['status'], str)
assert isinstance(result['created_at'], str)
assert isinstance(result['embed_token'], str)
assert isinstance(result['embed_url'], str)

# Check datetime serialization
try:
datetime.fromisoformat(result['deadline'])
datetime.fromisoformat(result['created_at'])
except ValueError as e:
pytest.fail(f"Invalid datetime format: {e}")

# Check embed_url format
assert result['embed_url'] == 'http://localhost:5000/embed/test-token'

def test_goal_to_dict_with_completed_at(session):
"""Test Goal.to_dict() with completed_at field."""
deadline = datetime.now(UTC) + timedelta(days=7)
completed_at = datetime.now(UTC)
goal = Goal(
user_github_id='12345',
description='Completed goal',
deadline=deadline,
repo_url='https://github.com/user/repo',
completion_condition='fix bug',
status='completed',
completed_at=completed_at,
repo_owner='user',
repo_name='repo',
embed_token='test-token-completed'
)
session.add(goal)
session.commit()

result = goal.to_dict()

# Check completed_at is serialized
assert 'completed_at' in result
assert result['completed_at'] is not None
assert isinstance(result['completed_at'], str)

# Check it's valid ISO format
try:
datetime.fromisoformat(result['completed_at'])
except ValueError as e:
pytest.fail(f"Invalid completed_at datetime format: {e}")

def test_goal_to_dict_without_embed_token(session):
"""Test Goal.to_dict() without embed_token."""
deadline = datetime.now(UTC) + timedelta(days=7)
goal = Goal(
user_github_id='12345',
description='Goal without embed token',
deadline=deadline,
repo_url='https://github.com/user/repo',
completion_condition='fix bug',
status='active',
repo_owner='user',
repo_name='repo'
)
session.add(goal)
session.commit()

result = goal.to_dict()

# Check embed_token and embed_url are None
assert result['embed_token'] is None
assert result['embed_url'] is None