mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-08 04:30:20 -06:00
531 lines
18 KiB
Python
531 lines
18 KiB
Python
"""Tests for REST API v1"""
|
|
import pytest
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from app import create_app, db
|
|
from app.models import User, Project, TimeEntry, Task, Client, ApiToken
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create and configure a test app instance"""
|
|
app = create_app({
|
|
'TESTING': True,
|
|
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
|
|
'WTF_CSRF_ENABLED': False
|
|
})
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.session.remove()
|
|
db.drop_all()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Test client"""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(app):
|
|
"""Create a test user"""
|
|
user = User(username='testuser', email='test@example.com')
|
|
user.set_password('password')
|
|
user.is_active = True
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_user(app):
|
|
"""Create an admin user"""
|
|
user = User(username='admin', email='admin@example.com', role='admin')
|
|
user.set_password('password')
|
|
user.is_active = True
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def api_token(app, test_user):
|
|
"""Create an API token with full permissions"""
|
|
token, plain_token = ApiToken.create_token(
|
|
user_id=test_user.id,
|
|
name='Test Token',
|
|
description='For testing',
|
|
scopes='read:projects,write:projects,read:time_entries,write:time_entries,read:tasks,write:tasks,read:clients,write:clients,read:reports,read:users'
|
|
)
|
|
db.session.add(token)
|
|
db.session.commit()
|
|
return plain_token
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(app, test_user, test_client_model):
|
|
"""Create a test project"""
|
|
project = Project(
|
|
name='Test Project',
|
|
description='A test project',
|
|
hourly_rate=75.0,
|
|
status='active',
|
|
client_id=test_client_model.id
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client_model(app):
|
|
"""Create a test client"""
|
|
client_model = Client(
|
|
name='Test Client',
|
|
email='client@example.com',
|
|
company='Test Company'
|
|
)
|
|
db.session.add(client_model)
|
|
db.session.commit()
|
|
return client_model
|
|
|
|
|
|
class TestAPIAuthentication:
|
|
"""Test API authentication"""
|
|
|
|
def test_no_token(self, client):
|
|
"""Test request without token"""
|
|
response = client.get('/api/v1/projects')
|
|
assert response.status_code == 401
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_invalid_token(self, client):
|
|
"""Test request with invalid token"""
|
|
headers = {'Authorization': 'Bearer invalid_token'}
|
|
response = client.get('/api/v1/projects', headers=headers)
|
|
assert response.status_code == 401
|
|
|
|
def test_valid_bearer_token(self, client, api_token):
|
|
"""Test request with valid Bearer token"""
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/projects', headers=headers)
|
|
assert response.status_code == 200
|
|
|
|
def test_valid_api_key_header(self, client, api_token):
|
|
"""Test request with valid X-API-Key header"""
|
|
headers = {'X-API-Key': api_token}
|
|
response = client.get('/api/v1/projects', headers=headers)
|
|
assert response.status_code == 200
|
|
|
|
def test_insufficient_scope(self, app, client, test_user):
|
|
"""Test request with insufficient scope"""
|
|
# Create token with limited scope
|
|
token, plain_token = ApiToken.create_token(
|
|
user_id=test_user.id,
|
|
name='Limited Token',
|
|
scopes='read:projects' # Only read access
|
|
)
|
|
db.session.add(token)
|
|
db.session.commit()
|
|
|
|
headers = {'Authorization': f'Bearer {plain_token}'}
|
|
|
|
# Should work for read
|
|
response = client.get('/api/v1/projects', headers=headers)
|
|
assert response.status_code == 200
|
|
|
|
# Should fail for write
|
|
response = client.post('/api/v1/projects',
|
|
json={'name': 'New Project'},
|
|
headers=headers)
|
|
assert response.status_code == 403
|
|
data = json.loads(response.data)
|
|
assert 'Insufficient permissions' in data['error']
|
|
|
|
|
|
class TestProjects:
|
|
"""Test project endpoints"""
|
|
|
|
def test_list_projects(self, client, api_token, test_project):
|
|
"""Test listing projects"""
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/projects', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'projects' in data
|
|
assert 'pagination' in data
|
|
assert len(data['projects']) == 1
|
|
assert data['projects'][0]['name'] == 'Test Project'
|
|
|
|
def test_get_project(self, client, api_token, test_project):
|
|
"""Test getting a single project"""
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get(f'/api/v1/projects/{test_project.id}', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'project' in data
|
|
assert data['project']['name'] == 'Test Project'
|
|
|
|
@pytest.mark.skip(reason="API endpoint returning 500 - needs investigation")
|
|
def test_create_project(self, client, api_token):
|
|
"""Test creating a project"""
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
project_data = {
|
|
'name': 'New Project',
|
|
'description': 'A new project',
|
|
'hourly_rate': 100.0,
|
|
'status': 'active'
|
|
}
|
|
|
|
response = client.post('/api/v1/projects',
|
|
json=project_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = json.loads(response.data)
|
|
assert 'project' in data
|
|
assert data['project']['name'] == 'New Project'
|
|
|
|
def test_update_project(self, client, api_token, test_project):
|
|
"""Test updating a project"""
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
update_data = {
|
|
'name': 'Updated Project',
|
|
'hourly_rate': 150.0
|
|
}
|
|
|
|
response = client.put(f'/api/v1/projects/{test_project.id}',
|
|
json=update_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['project']['name'] == 'Updated Project'
|
|
assert data['project']['hourly_rate'] == 150.0
|
|
|
|
def test_delete_project(self, client, api_token, test_project):
|
|
"""Test archiving a project"""
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.delete(f'/api/v1/projects/{test_project.id}',
|
|
headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify project is archived
|
|
project = Project.query.get(test_project.id)
|
|
assert project.status == 'archived'
|
|
|
|
|
|
class TestTimeEntries:
|
|
"""Test time entry endpoints"""
|
|
|
|
@pytest.mark.skip(reason="Transaction closed error - needs investigation")
|
|
def test_list_time_entries(self, client, api_token, test_user, test_project):
|
|
"""Test listing time entries"""
|
|
# Create a test time entry
|
|
entry = TimeEntry(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
start_time=datetime.utcnow() - timedelta(hours=2),
|
|
end_time=datetime.utcnow(),
|
|
source='api'
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/time-entries', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'time_entries' in data
|
|
assert len(data['time_entries']) == 1
|
|
|
|
def test_create_time_entry(self, client, api_token, test_project):
|
|
"""Test creating a time entry"""
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
entry_data = {
|
|
'project_id': test_project.id,
|
|
'start_time': '2024-01-15T09:00:00Z',
|
|
'end_time': '2024-01-15T17:00:00Z',
|
|
'notes': 'Development work',
|
|
'billable': True
|
|
}
|
|
|
|
response = client.post('/api/v1/time-entries',
|
|
json=entry_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = json.loads(response.data)
|
|
assert 'time_entry' in data
|
|
assert data['time_entry']['notes'] == 'Development work'
|
|
|
|
@pytest.mark.skip(reason="Transaction closed error - needs investigation")
|
|
def test_update_time_entry(self, client, api_token, test_user, test_project):
|
|
"""Test updating a time entry"""
|
|
# Create entry
|
|
entry = TimeEntry(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
start_time=datetime.utcnow() - timedelta(hours=2),
|
|
end_time=datetime.utcnow(),
|
|
notes='Original notes',
|
|
source='api'
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
update_data = {
|
|
'notes': 'Updated notes',
|
|
'billable': False
|
|
}
|
|
|
|
response = client.put(f'/api/v1/time-entries/{entry.id}',
|
|
json=update_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['time_entry']['notes'] == 'Updated notes'
|
|
assert data['time_entry']['billable'] == False
|
|
|
|
|
|
class TestTimer:
|
|
"""Test timer control endpoints"""
|
|
|
|
def test_get_timer_status_no_active(self, client, api_token):
|
|
"""Test getting timer status when no timer is active"""
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/timer/status', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['active'] == False
|
|
assert data['timer'] is None
|
|
|
|
def test_start_timer(self, client, api_token, test_project):
|
|
"""Test starting a timer"""
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
timer_data = {
|
|
'project_id': test_project.id
|
|
}
|
|
|
|
response = client.post('/api/v1/timer/start',
|
|
json=timer_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = json.loads(response.data)
|
|
assert 'timer' in data
|
|
assert data['timer']['project_id'] == test_project.id
|
|
|
|
@pytest.mark.skip(reason="Transaction closed error - needs investigation")
|
|
def test_stop_timer(self, client, api_token, test_user, test_project):
|
|
"""Test stopping a timer"""
|
|
# Start a timer
|
|
timer = TimeEntry(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
start_time=datetime.utcnow(),
|
|
source='api'
|
|
)
|
|
db.session.add(timer)
|
|
db.session.commit()
|
|
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.post('/api/v1/timer/stop', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'time_entry' in data
|
|
assert data['time_entry']['end_time'] is not None
|
|
|
|
|
|
class TestTasks:
|
|
"""Test task endpoints"""
|
|
|
|
@pytest.mark.skip(reason="Transaction closed error - needs investigation")
|
|
def test_list_tasks(self, client, api_token, test_project):
|
|
"""Test listing tasks"""
|
|
# Create a test task
|
|
task = Task(
|
|
name='Test Task',
|
|
project_id=test_project.id,
|
|
status='todo',
|
|
priority=1
|
|
)
|
|
db.session.add(task)
|
|
db.session.commit()
|
|
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/tasks', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'tasks' in data
|
|
assert len(data['tasks']) == 1
|
|
|
|
@pytest.mark.skip(reason="API endpoint returning 500 - needs investigation")
|
|
def test_create_task(self, client, api_token, test_project):
|
|
"""Test creating a task"""
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
task_data = {
|
|
'name': 'New Task',
|
|
'description': 'Task description',
|
|
'project_id': test_project.id,
|
|
'status': 'todo',
|
|
'priority': 1
|
|
}
|
|
|
|
response = client.post('/api/v1/tasks',
|
|
json=task_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = json.loads(response.data)
|
|
assert 'task' in data
|
|
assert data['task']['name'] == 'New Task'
|
|
|
|
|
|
class TestClients:
|
|
"""Test client endpoints"""
|
|
|
|
def test_list_clients(self, client, api_token, test_client_model):
|
|
"""Test listing clients"""
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/clients', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'clients' in data
|
|
assert len(data['clients']) == 1
|
|
|
|
def test_create_client(self, client, api_token):
|
|
"""Test creating a client"""
|
|
headers = {
|
|
'Authorization': f'Bearer {api_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
client_data = {
|
|
'name': 'New Client',
|
|
'email': 'newclient@example.com',
|
|
'company': 'New Company'
|
|
}
|
|
|
|
response = client.post('/api/v1/clients',
|
|
json=client_data,
|
|
headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = json.loads(response.data)
|
|
assert 'client' in data
|
|
assert data['client']['name'] == 'New Client'
|
|
|
|
|
|
class TestReports:
|
|
"""Test report endpoints"""
|
|
|
|
@pytest.mark.skip(reason="Transaction closed error - needs investigation")
|
|
def test_summary_report(self, client, api_token, test_user, test_project):
|
|
"""Test getting summary report"""
|
|
# Create some time entries
|
|
entry1 = TimeEntry(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
start_time=datetime.utcnow() - timedelta(hours=10),
|
|
end_time=datetime.utcnow() - timedelta(hours=8),
|
|
source='api'
|
|
)
|
|
entry2 = TimeEntry(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
start_time=datetime.utcnow() - timedelta(hours=5),
|
|
end_time=datetime.utcnow() - timedelta(hours=3),
|
|
billable=True,
|
|
source='api'
|
|
)
|
|
db.session.add_all([entry1, entry2])
|
|
db.session.commit()
|
|
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
response = client.get('/api/v1/reports/summary', headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'summary' in data
|
|
assert data['summary']['total_entries'] == 2
|
|
|
|
|
|
class TestPagination:
|
|
"""Test pagination"""
|
|
|
|
@pytest.mark.skip(reason="IntegrityError - needs investigation")
|
|
def test_pagination_params(self, client, api_token, test_project):
|
|
"""Test pagination parameters"""
|
|
# Create multiple projects
|
|
for i in range(15):
|
|
project = Project(
|
|
name=f'Project {i}',
|
|
status='active'
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
headers = {'Authorization': f'Bearer {api_token}'}
|
|
|
|
# Test per_page
|
|
response = client.get('/api/v1/projects?per_page=5', headers=headers)
|
|
data = json.loads(response.data)
|
|
assert len(data['projects']) == 5
|
|
assert data['pagination']['per_page'] == 5
|
|
|
|
# Test page
|
|
response = client.get('/api/v1/projects?page=2&per_page=5', headers=headers)
|
|
data = json.loads(response.data)
|
|
assert data['pagination']['page'] == 2
|
|
|
|
|
|
class TestSystemEndpoints:
|
|
"""Test system endpoints"""
|
|
|
|
def test_api_info(self, client):
|
|
"""Test API info endpoint (no auth required)"""
|
|
response = client.get('/api/v1/info')
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'api_version' in data
|
|
assert 'endpoints' in data
|
|
|
|
def test_health_check(self, client):
|
|
"""Test health check endpoint (no auth required)"""
|
|
response = client.get('/api/v1/health')
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['status'] == 'healthy'
|
|
|