bmc_hub/tests/test_work_desk.py
Christian 1fb5b4a191 Add comprehensive tests for email and drift functionalities
- Introduced tests for drift operations UI and backend to ensure proper handling of ignore rules and UI elements.
- Added tests for email identity API to validate email linking and identity retrieval without updates.
- Implemented tests for email purchase link service to verify correct handling of purchase mail links and conflict scenarios.
- Created tests for email reply automation to ensure proper routing and processing of replies based on classification.
- Developed tests for email sender creation to validate suggestions and handling of existing contacts and companies.
- Added tests for email sender identity to ensure correct matching of contacts and domains.
- Implemented tests for work desk functionalities to validate task management, meeting registrations, and timer functionalities.
2026-11-03 01:16:12 +01:00

179 lines
10 KiB
Python

import asyncio
from datetime import date, datetime, timedelta
from pathlib import Path
from uuid import uuid4
import pytest
from fastapi import HTTPException
from app.modules.planner.backend import desk, work, calendar
from test_unified_planning import planning_db
def row(**values):
return {'sag_id': 12, 'status': 'åben', 'work_status': 'ready', 'estimated_minutes': 120,
'actual_minutes': 30, 'future_planned_minutes': 30, 'planned_slots': [], **values}
def test_daily_selection_keeps_unfinished_work_and_future_appointments():
day = date(2026, 10, 21)
now = datetime(2026, 10, 21, 8, tzinfo=work.TZ)
assert desk.daily_task(row(start_date=day-timedelta(days=10)), day, now)['in_day']
assert not desk.daily_task(row(start_date=day+timedelta(days=1)), day, now)['in_day']
assert not desk.daily_task(row(work_status='done'), day, now)['in_day']
task = desk.daily_task(row(start_date=day, start_time=datetime.strptime('14:00','%H:%M').time()), day, now)
assert task['in_day'] and not task['ready']
def test_missing_time_is_individual_and_counts_future_bookings_once():
day = date(2026, 10, 21)
now = datetime(2026, 10, 21, 8, tzinfo=work.TZ)
task = desk.daily_task(row(), day, now)
assert task['remaining_work_minutes'] == 90 and task['missing_minutes'] == 60
unknown = desk.daily_task(row(estimated_minutes=None, case_estimated_minutes=600), day, now)
assert unknown['remaining_work_minutes'] is None and unknown['missing_minutes'] is None
def test_today_slots_are_clipped_at_midnight_and_deadline_precedes_ready_backlog():
day = date(2026, 10, 21)
now = datetime(2026, 10, 21, 8, tzinfo=work.TZ)
overnight = desk.daily_task(row(planned_slots=[{'starts_at':'2026-10-20T23:00:00+02:00','ends_at':'2026-10-21T01:00:00+02:00'}]), day, now)
assert overnight['planned_today_minutes'] == 60
overdue = desk.daily_task(row(deadline=now-timedelta(hours=1)), day, now)
assert overdue['_rank'] < desk.daily_task(row(), day, now)['_rank']
blocked = desk.daily_task(row(work_status='waiting'), day, now)
assert blocked['blocked_reason'] == 'Afventer'
too_late = desk.daily_task(row(deadline=now.replace(hour=16), planned_slots=[
{'starts_at':'2026-10-22T09:00:00+02:00','ends_at':'2026-10-22T10:00:00+02:00'}]), day, now)
assert too_late['booking_after_deadline']
assert 'tid reserveret efter deadline' in too_late['priority_reason']
def test_meeting_registration_requires_aware_positive_period_and_unique_people():
with pytest.raises(ValueError):
desk.MeetingAttendance(user_id=1,starts_at=datetime(2026,10,20),ends_at=datetime(2026,10,21))
with pytest.raises(ValueError):
desk.MeetingAttendance(user_id=1,starts_at=datetime(2026,10,20,tzinfo=work.TZ),ends_at=datetime(2026,10,19,tzinfo=work.TZ))
def test_min_queue_uses_desk_and_can_restore_legacy_view(monkeypatch):
from starlette.requests import Request
from app.modules.planner.frontend import views
monkeypatch.setattr(views.templates,'TemplateResponse',lambda name,context:name)
monkeypatch.setenv('PLANNING_UI_ENABLED','true')
monkeypatch.delenv('WORK_DESK_ENABLED',raising=False)
def view(query):
return asyncio.run(views.planning(Request({'type':'http','path':'/planning','query_string':query.encode(),'headers':[]}),{'id':1}))
assert view('view=queue').endswith('/desk.html')
assert view('view=tasks').endswith('/work.html')
monkeypatch.setenv('WORK_DESK_ENABLED','false')
assert view('view=queue').endswith('/work.html')
@pytest.fixture
def desk_db(planning_db):
from app.core.database import execute_query
execute_query(Path('migrations/1073_work_desk.sql').read_text(), fetch=False)
execute_query(Path('migrations/1073_work_desk.sql').read_text(), fetch=False)
return planning_db
def test_db_desk_order_estimate_contacts_and_seen(desk_db):
from app.core.database import execute_query, execute_query_single
actor, users, cases = desk_db
for cid in cases:
work.add_assignee(cid, work.AssignmentInput(user_id=actor['id']), actor)
today = datetime.now(work.TZ).date()
execute_query('UPDATE sag_sager SET start_date=%s,deadline=%s WHERE id=%s',
(today,datetime.now(work.TZ)-timedelta(days=1),cases[1]),fetch=False)
contact=execute_query_single("INSERT INTO contacts(first_name,last_name,email) VALUES('Desk','Test','desk@example.invalid') RETURNING id")
execute_query('INSERT INTO sag_kontakter(sag_id,contact_id,is_primary) VALUES(%s,%s,true)',(cases[1],contact['id']),fetch=False)
initial=desk.desk(user=actor)
assert initial['items'][0]['sag_id']==cases[1]
assert initial['items'][0]['contact']['name']=='Desk Test'
assert all(r['is_new'] for r in initial['items'])
manual=desk.desk_order(desk.DeskOrder(version=initial['version'],ids=list(reversed(cases))),actor)
assert [r['sag_id'] for r in desk.desk(user=actor)['items']]==list(reversed(cases))
with pytest.raises(HTTPException) as stale:
desk.desk_order(desk.DeskOrder(version=initial['version'],ids=cases),actor)
assert stale.value.status_code==409
updated=work.change_queue(actor['id'],cases[0],work.QueueChange(version=manual['version'],estimated_minutes=75),actor)
assert next(r for r in desk.desk(user=actor)['items'] if r['sag_id']==cases[0])['remaining_work_minutes']==75
desk.desk_order(desk.DeskOrder(version=updated['version'],mode='recommended'),actor)
assert desk.desk(user=actor)['items'][0]['sag_id']==cases[1]
desk.desk_seen(actor)
assert not any(r['is_new'] for r in desk.desk(user=actor)['items'])
def test_db_meeting_actual_time_replay_and_permission_are_atomic(desk_db):
from app.core.database import execute_query_single
actor, users, cases = desk_db
start=datetime.now(work.TZ)-timedelta(days=1,hours=2)
end=start+timedelta(hours=1)
meeting=calendar.create_event(calendar.EventInput(title='Desk meeting',kind='meeting',user_ids=users[:2],starts_at=start,ends_at=end,confirm_conflicts=True),actor)['event']
attendance=desk.MeetingRegistration(version=meeting['version'],attendees=[desk.MeetingAttendance(user_id=users[0],starts_at=start,ends_at=end),desk.MeetingAttendance(user_id=users[1],starts_at=start+timedelta(minutes=30),ends_at=end)])
first=desk.register_meeting(meeting['id'],attendance,actor)
assert desk.register_meeting(meeting['id'],attendance,actor)==first
count=execute_query_single('SELECT COUNT(*) AS n,SUM(faktisk_tid_min) AS minutes FROM tmodule_times WHERE planner_event_id=%s',(meeting['id'],))
assert count=={'n':2,'minutes':90}
changed=attendance.model_copy(deep=True)
changed.attendees[0].ends_at-=timedelta(minutes=15)
with pytest.raises(HTTPException) as duplicate:
desk.register_meeting(meeting['id'],changed,actor)
assert duplicate.value.status_code==409
assert execute_query_single('SELECT COUNT(*) AS n FROM tmodule_times WHERE planner_event_id=%s',(meeting['id'],))['n']==2
outsider={'id':users[2],'permissions':['planner.view','cases.view']}
with pytest.raises(HTTPException) as denied:
desk.meeting_attendance(meeting['id'],outsider)
assert denied.value.status_code==403
def test_db_standalone_timer_has_no_case_and_rejects_second_active_timer(desk_db):
actor, users, _ = desk_db
from app.core.database import execute_query_single
first=asyncio.run(desk.start_desk_timer(desk.DeskTimer(description='Desk standalone'),actor))
saved=execute_query_single('SELECT sag_id,billable,medarbejder_id,aktiv_timer FROM tmodule_times WHERE id=%s',(first['entry']['id'],))
assert saved=={'sag_id':None,'billable':False,'medarbejder_id':actor['id'],'aktiv_timer':True}
with pytest.raises(HTTPException) as duplicate:
asyncio.run(desk.start_desk_timer(desk.DeskTimer(description='Duplicate'),actor))
assert duplicate.value.status_code==409
def test_db_meeting_rejects_future_actual_time(desk_db):
actor, users, _ = desk_db
start=datetime.now(work.TZ)+timedelta(days=1)
meeting=calendar.create_event(calendar.EventInput(title='Future meeting',kind='meeting',user_ids=[users[0]],starts_at=start,ends_at=start+timedelta(hours=1),confirm_conflicts=True),actor)['event']
with pytest.raises(HTTPException) as future:
desk.register_meeting(meeting['id'],desk.MeetingRegistration(version=meeting['version'],attendees=[desk.MeetingAttendance(user_id=users[0],starts_at=start,ends_at=start+timedelta(hours=1))]),actor)
assert future.value.status_code==400
def test_db_case_time_retry_does_not_duplicate_entry(desk_db):
actor, _, cases = desk_db
from app.core.database import execute_query_single
payload=desk.DeskTime(request_id=uuid4(),sag_id=cases[0],description='Desk case time',
starts_at=datetime.now(work.TZ)-timedelta(minutes=20),minutes=15)
first=asyncio.run(desk.register_case_time(payload,actor))
assert asyncio.run(desk.register_case_time(payload,actor))==first
assert execute_query_single('SELECT COUNT(*) AS n FROM tmodule_times WHERE sag_id=%s AND description=%s',
(cases[0],payload.description))['n']==1
with pytest.raises(HTTPException) as changed:
asyncio.run(desk.register_case_time(payload.model_copy(update={'minutes':30}),actor))
assert changed.value.status_code==409
def test_db_today_reservations_do_not_double_count_and_keep_simultaneous_events(desk_db):
actor, users, cases = desk_db
start=datetime.now(work.TZ).replace(hour=14,minute=0,second=0,microsecond=0)
end=start+timedelta(hours=1)
for _ in range(2):
calendar.create_event(calendar.EventInput(title='Two reservations',sag_id=cases[0],user_ids=[users[0]],
starts_at=start,ends_at=end,confirm_conflicts=True),actor)
result=desk.desk(user=actor)
task=next(r for r in result['items'] if r['sag_id']==cases[0])
assert len(task['today_slots'])==2
assert task['planned_today_minutes']==120
before=result['stored_order_ids']
manual=desk.desk_order(desk.DeskOrder(version=result['version'],ids=list(reversed(before))),actor)
desk.desk_order(desk.DeskOrder(version=manual['version'],mode='recommended',ids=before),actor)
assert desk.desk(user=actor)['stored_order_ids']==before