Files
TimeTracker/tests/test_single_active_timer_setting.py
T
Dries Peeters 6c8e86cd01 fix(timer): respect Settings.single_active_timer at runtime
Timer starts always blocked a second running entry and never read the\nadmin-controlled Settings flag.\n\n- Add TimeTrackingService.can_start_timer() using Settings.get_settings()\n  and wire it into start_timer, web timer routes, kiosk start, and\n  legacy POST /api/timer/resume.\n- POST /api/v1/timer/start returns 409 with error_code\n  timer_already_running when single-active mode is on and a timer\n  is already running.\n- Deduplicate start_timer template handling in the service.\n\nTests: tests/test_single_active_timer_setting.py.\nDocs: REST_API (responses), GETTING_STARTED, REQUIREMENTS, Docker env\nnotes, TESTING_STRATEGY, env.example comment; CHANGELOG entry.
2026-04-27 19:16:25 +02:00

145 lines
4.2 KiB
Python

"""Tests for Settings.single_active_timer enforcement (DB) vs env defaults."""
import json
from datetime import datetime
from decimal import Decimal
import pytest
from app import db
from app.models import Project, Settings, TimeEntry
pytestmark = [pytest.mark.integration]
def _api_headers(plain_token: str) -> dict:
return {"Authorization": f"Bearer {plain_token}", "Content-Type": "application/json"}
def _second_project(client_id: int) -> Project:
p = Project(
name="Second Timer Project",
client_id=client_id,
description="second",
billable=True,
hourly_rate=Decimal("80.00"),
status="active",
)
db.session.add(p)
db.session.flush()
return p
def test_single_timer_enforced_when_setting_on(app, client, user, project, api_token):
_, plain_token = api_token
p2 = _second_project(project.client_id)
db.session.commit()
with app.app_context():
settings = Settings.get_settings()
settings.single_active_timer = True
db.session.commit()
running = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=None,
source="manual",
billable=True,
)
db.session.add(running)
db.session.commit()
resp = client.post(
"/api/v1/timer/start",
json={"project_id": p2.id},
headers=_api_headers(plain_token),
)
assert resp.status_code == 409
data = json.loads(resp.data)
assert data.get("success") is False
assert data.get("error_code") == "timer_already_running"
def test_multiple_timers_allowed_when_setting_off(app, client, user, project, api_token):
_, plain_token = api_token
p2 = _second_project(project.client_id)
db.session.commit()
with app.app_context():
settings = Settings.get_settings()
settings.single_active_timer = False
db.session.commit()
r1 = client.post("/api/v1/timer/start", json={"project_id": project.id}, headers=_api_headers(plain_token))
assert r1.status_code == 201
r2 = client.post("/api/v1/timer/start", json={"project_id": p2.id}, headers=_api_headers(plain_token))
assert r2.status_code == 201
with app.app_context():
active = TimeEntry.query.filter_by(user_id=user.id, end_time=None).all()
assert len(active) == 2
def test_setting_read_from_db_not_env(app, client, user, project, api_token):
"""DB single_active_timer=False must allow a second timer even if env default is restrictive."""
_, plain_token = api_token
p2 = _second_project(project.client_id)
db.session.commit()
with app.app_context():
settings = Settings.get_settings()
settings.single_active_timer = False
db.session.commit()
running = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=None,
source="manual",
billable=True,
)
db.session.add(running)
db.session.commit()
resp = client.post(
"/api/v1/timer/start",
json={"project_id": p2.id},
headers=_api_headers(plain_token),
)
assert resp.status_code == 201
with app.app_context():
assert TimeEntry.query.filter_by(user_id=user.id, end_time=None).count() == 2
def test_both_web_and_api_routes_respect_setting(app, authenticated_client, user, project, api_token):
_, plain_token = api_token
p2 = _second_project(project.client_id)
db.session.commit()
with app.app_context():
settings = Settings.get_settings()
settings.single_active_timer = False
db.session.commit()
web_resp = authenticated_client.post(
"/timer/start",
data={"project_id": str(project.id)},
follow_redirects=True,
)
assert web_resp.status_code == 200
api_resp = authenticated_client.post(
"/api/v1/timer/start",
json={"project_id": p2.id},
headers=_api_headers(plain_token),
)
assert api_resp.status_code == 201
with app.app_context():
active = TimeEntry.query.filter_by(user_id=user.id, end_time=None).all()
assert len(active) == 2