mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 05:10:26 -05:00
ef9b66f5e7
Global search referenced Client.company, which is not a column on Client, so client matches failed at runtime. Legacy and v1 search, plus search_clients(), now filter on name, email, description, and contact_person; result descriptions use the same fields. Legacy /api/search returns count: 0 for queries shorter than two characters so responses stay consistent. OpenAPI info.version is taken from get_version_from_setup(), with a config fallback when the resolved version is unknown. get_version_from_setup() also honors TIMETRACKER_VERSION and APP_VERSION for CI and container builds. Client.__init__ accepts custom_fields. ClientService no longer passes status= into Client(), which the initializer does not support. Tests add HTTP route contract checks and OpenAPI version alignment, fix subcontractor search fixtures (Client/Task construction and v1 client fixture naming), and update related API integration tests.
780 lines
26 KiB
Python
780 lines
26 KiB
Python
"""
|
|
Test suite for route/endpoint testing.
|
|
Tests all major routes and API endpoints.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
|
|
|
|
# ============================================================================
|
|
# Smoke Tests - Critical Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.routes
|
|
def test_health_check(client):
|
|
"""Test health check endpoint - critical for deployment."""
|
|
response = client.get("/_health")
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert data["status"] == "healthy"
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.routes
|
|
def test_login_page_accessible(client):
|
|
"""Test that login page is accessible."""
|
|
response = client.get("/login")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.routes
|
|
def test_static_files_accessible(client):
|
|
"""Test that static files can be accessed."""
|
|
# Test CSS
|
|
response = client.get("/static/css/style.css")
|
|
# 200 if exists, 404 if not - both are acceptable
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
# ============================================================================
|
|
# Authentication Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.routes
|
|
def test_protected_route_redirects_to_login(client):
|
|
"""Test that protected routes redirect unauthenticated users."""
|
|
response = client.get("/dashboard", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert "/login" in response.location or "login" in response.location.lower()
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.routes
|
|
def test_dashboard_accessible_when_authenticated(authenticated_client):
|
|
"""Test that dashboard is accessible for authenticated users."""
|
|
response = authenticated_client.get("/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.routes
|
|
def test_logout_route(authenticated_client):
|
|
"""Test logout functionality."""
|
|
response = authenticated_client.get("/logout", follow_redirects=False)
|
|
assert response.status_code in [302, 200] # Redirect after logout
|
|
|
|
|
|
# ============================================================================
|
|
# Timer Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_start_timer_api(authenticated_client, project, app):
|
|
"""Test starting a timer via API."""
|
|
with app.app_context():
|
|
response = authenticated_client.post("/api/timer/start", json={"project_id": project.id})
|
|
|
|
# Accept both 200 and 201 as valid responses
|
|
assert response.status_code in [200, 201]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_stop_timer_api(authenticated_client, active_timer, app):
|
|
"""Test stopping a timer via session API (POST /api/timer/stop)."""
|
|
with app.app_context():
|
|
response = authenticated_client.post("/api/timer/stop")
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert data.get("success") is True
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_get_active_timer(authenticated_client, active_timer, app):
|
|
"""Test getting active timer status (GET /api/timer/status)."""
|
|
with app.app_context():
|
|
response = authenticated_client.get("/api/timer/status")
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert data.get("active") is True
|
|
assert data.get("timer") is not None
|
|
|
|
|
|
# ============================================================================
|
|
# Project Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_projects_list_page(authenticated_client):
|
|
"""Test projects list page."""
|
|
response = authenticated_client.get("/projects")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_projects_create_page_contains_client_modal_trigger(admin_authenticated_client):
|
|
"""Projects create page should contain inline client creation trigger."""
|
|
response = admin_authenticated_client.get("/projects/create")
|
|
assert response.status_code == 200
|
|
html = response.get_data(as_text=True)
|
|
assert 'id="openCreateClientModal"' in html
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_create_project_post_does_not_500_and_logs_activity(admin_authenticated_client, test_client, app):
|
|
"""Regression: creating a project should not 500 due to project.client being a string property."""
|
|
from app import db
|
|
from app.models import Project, Activity
|
|
|
|
with app.app_context():
|
|
name = "Project Name test"
|
|
resp = admin_authenticated_client.post(
|
|
"/projects/create",
|
|
data={
|
|
"name": name,
|
|
"client_id": str(test_client.id),
|
|
"description": "Created via test",
|
|
"billable": "",
|
|
"hourly_rate": "",
|
|
"billing_ref": "",
|
|
"budget_amount": "",
|
|
"budget_threshold_percent": "80",
|
|
"code": "",
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
|
|
# On success we redirect to the project page
|
|
assert resp.status_code in (302, 303)
|
|
|
|
created = Project.query.filter_by(name=name).order_by(Project.id.desc()).first()
|
|
assert created is not None
|
|
assert created.client_id == test_client.id
|
|
|
|
# Ensure we wrote the activity entry with a description that includes the client name
|
|
db.session.expire_all()
|
|
activity = (
|
|
Activity.query.filter_by(entity_type="project", entity_id=created.id, action="created")
|
|
.order_by(Activity.id.desc())
|
|
.first()
|
|
)
|
|
assert activity is not None
|
|
assert test_client.name in (activity.description or "")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_project_create_page(admin_authenticated_client):
|
|
"""Test project creation page (requires create_projects permission)."""
|
|
response = admin_authenticated_client.get("/projects/create")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_project_detail_page(authenticated_client, project, app):
|
|
"""Test project detail page."""
|
|
with app.app_context():
|
|
response = authenticated_client.get(f"/projects/{project.id}")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_create_project_api(client_with_token, test_client, app):
|
|
"""Test creating a project via API v1 (Bearer token)."""
|
|
response = client_with_token.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "API Test Project",
|
|
"client_id": test_client.id,
|
|
"description": "Created via API test",
|
|
"billable": True,
|
|
"hourly_rate": 85.00,
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.get_json()
|
|
assert data.get("project", {}).get("name") == "API Test Project"
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_edit_project_description(admin_authenticated_client, project, app):
|
|
"""Test that project description changes are saved correctly."""
|
|
from app.models import Project
|
|
from app import db
|
|
|
|
with app.app_context():
|
|
# Get the project ID
|
|
project_id = project.id
|
|
|
|
# Verify initial description
|
|
initial_description = project.description or ""
|
|
|
|
# New description to test
|
|
new_description = "This is an updated project description with markdown **bold** and *italic* text."
|
|
|
|
# POST to edit project with updated description
|
|
response = admin_authenticated_client.post(
|
|
f"/projects/{project_id}/edit",
|
|
data={
|
|
"name": project.name,
|
|
"client_id": project.client_id,
|
|
"description": new_description,
|
|
"billable": "on" if project.billable else "",
|
|
"hourly_rate": str(project.hourly_rate) if project.hourly_rate else "",
|
|
"billing_ref": project.billing_ref or "",
|
|
"code": project.code or "",
|
|
"budget_amount": str(project.budget_amount) if project.budget_amount else "",
|
|
"budget_threshold_percent": str(project.budget_threshold_percent or 80),
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
|
|
# Should redirect on success
|
|
assert response.status_code == 302
|
|
|
|
# Verify the description was saved in the database
|
|
db.session.expire_all() # Clear session cache
|
|
# Query fresh from database instead of refreshing fixture object
|
|
updated_project = Project.query.get(project_id)
|
|
assert updated_project is not None
|
|
assert updated_project.description == new_description
|
|
assert updated_project.description != initial_description
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.routes
|
|
def test_project_edit_page_has_markdown_editor(admin_authenticated_client, project):
|
|
"""Smoke test: Verify project edit page loads with markdown editor."""
|
|
response = admin_authenticated_client.get(f"/projects/{project.id}/edit")
|
|
assert response.status_code == 200
|
|
|
|
html = response.get_data(as_text=True)
|
|
|
|
# Verify the description textarea is present
|
|
assert 'id="description"' in html
|
|
assert 'name="description"' in html
|
|
|
|
# Verify markdown editor div is present
|
|
assert 'id="description_editor"' in html
|
|
|
|
# Verify ToastUI editor is loaded
|
|
assert "toastui-editor" in html.lower() or "toast.ui" in html.lower()
|
|
|
|
# Verify form submit handler is present to sync markdown editor
|
|
assert "descriptionInput.value = mdEditor.getMarkdown()" in html or "getMarkdown()" in html
|
|
|
|
|
|
# ============================================================================
|
|
# Client Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_clients_list_page(authenticated_client):
|
|
"""Test clients list page."""
|
|
response = authenticated_client.get("/clients")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_client_detail_page(authenticated_client, test_client, app):
|
|
"""Test client detail page."""
|
|
with app.app_context():
|
|
response = authenticated_client.get(f"/clients/{test_client.id}")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_edit_client_updates_prepaid_fields(admin_authenticated_client, test_client, app):
|
|
"""Ensure editing a client updates prepaid hours fields without errors."""
|
|
from app import db
|
|
from app.models import Client
|
|
|
|
with app.app_context():
|
|
client_id = test_client.id
|
|
|
|
response = admin_authenticated_client.post(
|
|
f"/clients/{client_id}/edit",
|
|
data={
|
|
"name": test_client.name,
|
|
"description": test_client.description or "",
|
|
"contact_person": test_client.contact_person or "",
|
|
"email": test_client.email or "",
|
|
"phone": test_client.phone or "",
|
|
"address": test_client.address or "",
|
|
"default_hourly_rate": "",
|
|
"prepaid_hours_monthly": "12.5",
|
|
"prepaid_reset_day": "10",
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
|
|
db.session.expire_all()
|
|
# Query fresh from database instead of refreshing fixture object
|
|
updated = Client.query.get(client_id)
|
|
assert updated is not None
|
|
assert updated.prepaid_hours_monthly == Decimal("12.5")
|
|
assert updated.prepaid_reset_day == 10
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_edit_client_rejects_negative_prepaid_hours(admin_authenticated_client, test_client, app):
|
|
"""Regression test: negative prepaid hours should trigger validation error."""
|
|
from app import db
|
|
from app.models import Client
|
|
|
|
with app.app_context():
|
|
client_id = test_client.id
|
|
db.session.expire_all()
|
|
baseline = Client.query.get(client_id)
|
|
baseline_hours = baseline.prepaid_hours_monthly
|
|
baseline_reset_day = baseline.prepaid_reset_day
|
|
|
|
response = admin_authenticated_client.post(
|
|
f"/clients/{client_id}/edit",
|
|
data={
|
|
"name": test_client.name,
|
|
"description": test_client.description or "",
|
|
"contact_person": test_client.contact_person or "",
|
|
"email": test_client.email or "",
|
|
"phone": test_client.phone or "",
|
|
"address": test_client.address or "",
|
|
"default_hourly_rate": "",
|
|
"prepaid_hours_monthly": "-1",
|
|
"prepaid_reset_day": "3",
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
|
|
# View should re-render with validation error (200 OK) or redirect back
|
|
# If it redirects, follow it to see the error message
|
|
if response.status_code == 302:
|
|
response = admin_authenticated_client.get(response.location, follow_redirects=True)
|
|
assert response.status_code == 200
|
|
|
|
db.session.expire_all()
|
|
updated = Client.query.get(client_id)
|
|
assert updated.prepaid_hours_monthly == baseline_hours
|
|
assert updated.prepaid_reset_day == baseline_reset_day
|
|
|
|
|
|
# ============================================================================
|
|
# Reports Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_reports_page(authenticated_client):
|
|
"""Test reports page."""
|
|
response = authenticated_client.get("/reports")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_time_report_api(authenticated_client, multiple_time_entries, app):
|
|
"""Test analytics hours-by-day API (replaces removed /api/reports/time)."""
|
|
with app.app_context():
|
|
response = authenticated_client.get("/api/analytics/hours-by-day", query_string={"days": 30})
|
|
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert "labels" in data
|
|
assert "datasets" in data
|
|
|
|
|
|
# ============================================================================
|
|
# Analytics Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_analytics_page(authenticated_client):
|
|
"""Test analytics dashboard page."""
|
|
response = authenticated_client.get("/analytics")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_dashboard_contains_start_timer_modal(authenticated_client):
|
|
"""Dashboard should render Start Timer modal container in new UI."""
|
|
response = authenticated_client.get("/dashboard")
|
|
assert response.status_code == 200
|
|
html = response.get_data(as_text=True)
|
|
assert 'id="startTimerModal"' in html
|
|
assert 'id="openStartTimer"' in html
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.routes
|
|
def test_base_layout_has_sidebar_toggle(authenticated_client):
|
|
"""Ensure sidebar collapse toggle is present on pages."""
|
|
response = authenticated_client.get("/dashboard")
|
|
assert response.status_code == 200
|
|
html = response.get_data(as_text=True)
|
|
assert 'id="sidebarCollapseBtn"' in html
|
|
assert 'id="mobileSidebarBtn"' in html
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_hours_by_day_api(authenticated_client, multiple_time_entries, app):
|
|
"""Test hours by day analytics API."""
|
|
with app.app_context():
|
|
response = authenticated_client.get("/api/analytics/hours-by-day", query_string={"days": 7})
|
|
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert "labels" in data
|
|
assert "datasets" in data
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_hours_by_project_api(authenticated_client, multiple_time_entries, app):
|
|
"""Test hours by project analytics API."""
|
|
with app.app_context():
|
|
response = authenticated_client.get("/api/analytics/hours-by-project", query_string={"days": 7})
|
|
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert "labels" in data
|
|
assert "datasets" in data
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_invoices_list_page(authenticated_client):
|
|
"""Test invoices list page."""
|
|
response = authenticated_client.get("/invoices")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_invoice_detail_page(authenticated_client, invoice, app):
|
|
"""Test invoice detail page."""
|
|
with app.app_context():
|
|
response = authenticated_client.get(f"/invoices/{invoice.id}")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_invoice_create_page(authenticated_client):
|
|
"""Test invoice creation page."""
|
|
response = authenticated_client.get("/invoices/create")
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ============================================================================
|
|
# Admin Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_admin_page_requires_admin(authenticated_client):
|
|
"""Test that admin pages require admin role."""
|
|
response = authenticated_client.get("/admin", follow_redirects=False)
|
|
# Should redirect or return 403
|
|
assert response.status_code in [302, 403]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_admin_page_accessible_by_admin(admin_authenticated_client):
|
|
"""Test that admin pages are accessible by admins."""
|
|
response = admin_authenticated_client.get("/admin")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_admin_users_list(admin_authenticated_client):
|
|
"""Test admin users list page."""
|
|
response = admin_authenticated_client.get("/admin/users")
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ============================================================================
|
|
# Error Pages
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.routes
|
|
def test_404_error_page(client):
|
|
"""Test 404 error page."""
|
|
response = client.get("/this-page-does-not-exist")
|
|
assert response.status_code == 404
|
|
|
|
|
|
# ============================================================================
|
|
# API Validation Tests
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
def test_api_requires_authentication(client):
|
|
"""Test that API endpoints require authentication."""
|
|
response = client.get("/api/timer/status")
|
|
assert response.status_code in [302, 401, 403]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
def test_api_invalid_json(authenticated_client):
|
|
"""Test API with invalid JSON."""
|
|
response = authenticated_client.post("/api/timer/start", data="invalid json", content_type="application/json")
|
|
# Should return 400 or 422 for bad request
|
|
assert response.status_code in [400, 422, 500] # Depending on error handling
|
|
|
|
|
|
# ============================================================================
|
|
# Settings Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_settings_page(authenticated_client):
|
|
"""Test settings page."""
|
|
response = authenticated_client.get("/settings")
|
|
# Settings might be at different URL
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
# ============================================================================
|
|
# Task Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_tasks_list_page(authenticated_client):
|
|
"""Test tasks list page."""
|
|
response = authenticated_client.get("/tasks")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_task_create_page(authenticated_client, project, app):
|
|
"""Test task creation page."""
|
|
with app.app_context():
|
|
response = authenticated_client.get(f"/tasks/create?project_id={project.id}")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_task_detail_page(authenticated_client, task, app):
|
|
"""Test task detail page."""
|
|
with app.app_context():
|
|
response = authenticated_client.get(f"/tasks/{task.id}")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_create_task_api(authenticated_client, project, app):
|
|
"""Test creating a task via POST /api/tasks/create."""
|
|
with app.app_context():
|
|
response = authenticated_client.post(
|
|
"/api/tasks/create",
|
|
json={
|
|
"name": "API Test Task",
|
|
"project_id": project.id,
|
|
"description": "Created via API test",
|
|
"priority": "medium",
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.get_json()
|
|
assert data.get("success") is True
|
|
assert data.get("name") == "API Test Task"
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_update_task_status_api_put(authenticated_client, task, app):
|
|
"""Test updating task status via API using PUT (current behavior)."""
|
|
with app.app_context():
|
|
response = authenticated_client.put(f"/api/tasks/{task.id}/status", json={"status": "in_progress"})
|
|
assert response.status_code in [200, 400, 403, 404]
|
|
if response.status_code == 200:
|
|
data = response.get_json()
|
|
assert data.get("success") is True
|
|
assert data.get("task", {}).get("status") == "in_progress"
|
|
|
|
|
|
# ============================================================================
|
|
# Comment Routes (if they exist)
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_add_comment_api(authenticated_client, task, app):
|
|
"""Test adding a comment via API."""
|
|
with app.app_context():
|
|
response = authenticated_client.post(f"/api/comments", json={"task_id": task.id, "content": "Test comment"})
|
|
# May not exist or require different structure
|
|
assert response.status_code in [200, 201, 400, 404, 405]
|
|
|
|
|
|
# ============================================================================
|
|
# Time Entry Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_time_entries_page(authenticated_client):
|
|
"""Test time entries page."""
|
|
response = authenticated_client.get("/time-entries")
|
|
# May be at different URL or part of dashboard
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_create_time_entry_api(authenticated_client, project, user, app):
|
|
"""Test creating a time entry via API."""
|
|
with app.app_context():
|
|
from datetime import datetime, timedelta
|
|
|
|
start_time = datetime.utcnow() - timedelta(hours=2)
|
|
end_time = datetime.utcnow()
|
|
|
|
response = authenticated_client.post(
|
|
"/api/time-entries",
|
|
json={
|
|
"project_id": project.id,
|
|
"start_time": start_time.isoformat(),
|
|
"end_time": end_time.isoformat(),
|
|
"notes": "API test entry",
|
|
},
|
|
)
|
|
assert response.status_code in [200, 201, 400, 404]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_update_time_entry_api(authenticated_client, time_entry, app):
|
|
"""Test updating a time entry via API."""
|
|
with app.app_context():
|
|
response = authenticated_client.put(f"/api/time-entries/{time_entry.id}", json={"notes": "Updated notes"})
|
|
assert response.status_code in [200, 400, 404]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
@pytest.mark.api
|
|
def test_delete_time_entry_api(authenticated_client, time_entry, app):
|
|
"""Test deleting a time entry via API."""
|
|
with app.app_context():
|
|
response = authenticated_client.delete(f"/api/time-entries/{time_entry.id}")
|
|
assert response.status_code in [200, 204, 404]
|
|
|
|
|
|
# ============================================================================
|
|
# User Profile Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_user_profile_page(authenticated_client):
|
|
"""Test user profile page."""
|
|
response = authenticated_client.get("/profile")
|
|
# May be at different URL
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_user_settings_page(authenticated_client):
|
|
"""Test user settings page."""
|
|
response = authenticated_client.get("/user/settings")
|
|
# May be at different URL
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
# ============================================================================
|
|
# Export Routes
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_export_time_entries_csv(authenticated_client, multiple_time_entries, app):
|
|
"""Test exporting time entries as CSV."""
|
|
with app.app_context():
|
|
from datetime import datetime, timedelta
|
|
|
|
response = authenticated_client.get(
|
|
"/reports/export/csv",
|
|
query_string={
|
|
"start_date": (datetime.utcnow() - timedelta(days=30)).strftime("%Y-%m-%d"),
|
|
"end_date": datetime.utcnow().strftime("%Y-%m-%d"),
|
|
},
|
|
)
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.routes
|
|
def test_export_invoice_pdf(authenticated_client, invoice_with_items, app):
|
|
"""Test exporting invoice as PDF."""
|
|
with app.app_context():
|
|
invoice, _ = invoice_with_items
|
|
response = authenticated_client.get(f"/invoices/{invoice.id}/pdf")
|
|
# PDF generation might not be available in all environments
|
|
assert response.status_code in [200, 404, 500]
|