Files
TimeTracker/tests/test_integration/test_caldav_integration.py
T
MacJediWizard ed934100a0 fix(test): refresh stale integration tests after connector refactors
Four integration tests fell behind code changes in app/integrations/ and
app/models. They were asserting against the old data shapes / patching
the old mock targets and only surfaced now that v5.5.6's fixture refresh
re-enabled them.

### tests/test_integration/test_caldav_integration.py
test_sync_data_imports_events still expected the connector to write
TimeEntry rows and IntegrationExternalEventLink rows. The connector
in app/integrations/caldav_calendar.py:803 now writes CalendarEvent
rows and tracks imports via a [CalDAV: <uid>] marker embedded in the
description. Update the assertions to query CalendarEvent and look for
the marker. Add CalendarEvent to the model imports.

### tests/test_integration/test_activitywatch_integration.py
Four tests in this file patched `app.integrations.activitywatch.requests.get`.
The connector at app/integrations/activitywatch.py:78-79 was refactored
to route HTTP through `integration_session()` + `session_request()`,
so requests.get is never called and the mocks never fire. Tests that
relied on the mock then attempted real HTTP to localhost:5600 and
failed.

Repoint all four @patch decorators to
`app.integrations.activitywatch.session_request`. Update the one
positional-argument assertion in test_test_connection_success — the
URL moves from arg[0] to arg[2] because session_request's signature
is (session, method, url, **kw).

### tests/test_custom_field_definitions.py
test_delete_custom_field_preserves_other_fields called
`test_client.set_custom_field(...)` directly on the fixture instance
which may belong to a different SQLAlchemy session by the time the
test runs. The two JSON mutations weren't reliably persisted, so only
one of the two fields survived to the assertions. Re-query the client
via `Client.query.get(test_client.id)` before mutating — matches the
pattern already used in the surrounding tests in the same file.

Test plan
- pytest tests/test_integration/test_caldav_integration.py::TestCalDAVConnector::test_sync_data_imports_events
- pytest tests/test_integration/test_activitywatch_integration.py::TestActivityWatchConnector
- pytest tests/test_custom_field_definitions.py::test_delete_custom_field_preserves_other_fields
2026-05-14 16:58:52 -04:00

409 lines
14 KiB
Python

"""
Tests for CalDAV calendar integration.
"""
import pytest
pytestmark = [pytest.mark.integration]
from datetime import datetime, timedelta, timezone
from unittest.mock import Mock, patch
from flask import url_for
from app.models import (
CalendarEvent,
Integration,
IntegrationCredential,
IntegrationExternalEventLink,
Project,
TimeEntry,
User,
)
from app.integrations.caldav_calendar import (
CalDAVCalendarConnector,
CalDAVClient,
CalDAVCalendar,
)
@pytest.fixture
def test_user(db_session):
"""Create a test user"""
user = User(username="testuser", email="test@example.com", role="admin")
user.set_password("testpass")
db_session.add(user)
db_session.commit()
return user
@pytest.fixture
def test_project(db_session, test_user):
"""Create a test project"""
from app.models import Client
client = Client(name="Test Client", email="client@example.com")
db_session.add(client)
db_session.commit()
project = Project(name="Test Project", client_id=client.id, status="active")
db_session.add(project)
db_session.commit()
return project
@pytest.fixture
def caldav_integration(db_session, test_user, test_project):
"""Create a CalDAV integration"""
integration = Integration(
name="CalDAV Calendar",
provider="caldav_calendar",
user_id=test_user.id,
is_global=False,
is_active=True,
config={
"server_url": "https://mail.example.com/dav",
"calendar_url": "https://mail.example.com/dav/user@example.com/Calendar/",
"calendar_name": "My Calendar",
"default_project_id": test_project.id,
"sync_direction": "calendar_to_time_tracker",
"lookback_days": 90,
"verify_ssl": True,
},
)
db_session.add(integration)
db_session.commit()
credentials = IntegrationCredential(
integration_id=integration.id,
access_token="test_password",
token_type="Basic",
scope="caldav",
extra_data={"username": "user@example.com"},
)
db_session.add(credentials)
db_session.commit()
return integration
class TestCalDAVClient:
"""Test CalDAV client functionality"""
def test_client_initialization(self):
"""Test CalDAV client can be initialized"""
client = CalDAVClient(
username="user@example.com", password="pass", verify_ssl=True
)
assert client.username == "user@example.com"
assert client.password == "pass"
assert client.verify_ssl is True
@patch("app.integrations.caldav_calendar.requests.request")
def test_discover_calendars(self, mock_request):
"""Test calendar discovery"""
# Mock PROPFIND responses
mock_resp1 = Mock()
mock_resp1.text = """<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:propstat>
<d:prop>
<d:current-user-principal>
<d:href>/dav/user@example.com</d:href>
</d:current-user-principal>
</d:prop>
</d:propstat>
</d:response>
</d:multistatus>"""
mock_resp1.raise_for_status = Mock()
mock_resp2 = Mock()
mock_resp2.text = """<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:cs="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:propstat>
<d:prop>
<cs:calendar-home-set>
<d:href>/dav/user@example.com/Calendar/</d:href>
</cs:calendar-home-set>
</d:prop>
</d:propstat>
</d:response>
</d:multistatus>"""
mock_resp2.raise_for_status = Mock()
mock_resp3 = Mock()
mock_resp3.text = """<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:cs="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>/dav/user@example.com/Calendar/</d:href>
<d:propstat>
<d:prop>
<d:displayname>My Calendar</d:displayname>
<d:resourcetype>
<cs:calendar/>
</d:resourcetype>
</d:prop>
</d:propstat>
</d:response>
</d:multistatus>"""
mock_resp3.raise_for_status = Mock()
mock_request.side_effect = [mock_resp1, mock_resp2, mock_resp3]
client = CalDAVClient(username="user@example.com", password="pass")
calendars = client.discover_calendars("https://mail.example.com/dav")
assert len(calendars) == 1
assert calendars[0].name == "My Calendar"
assert "Calendar" in calendars[0].href
class TestCalDAVConnector:
"""Test CalDAV connector"""
def test_provider_name(self, caldav_integration):
"""Test provider name"""
credentials = IntegrationCredential.query.filter_by(
integration_id=caldav_integration.id
).first()
connector = CalDAVCalendarConnector(caldav_integration, credentials)
assert connector.provider_name == "caldav_calendar"
def test_get_basic_creds(self, caldav_integration):
"""Test getting basic credentials"""
credentials = IntegrationCredential.query.filter_by(
integration_id=caldav_integration.id
).first()
connector = CalDAVCalendarConnector(caldav_integration, credentials)
username, password = connector._get_basic_creds()
assert username == "user@example.com"
assert password == "test_password"
def test_get_basic_creds_missing(self, caldav_integration):
"""Test getting credentials when missing"""
connector = CalDAVCalendarConnector(caldav_integration, None)
with pytest.raises(ValueError, match="Missing CalDAV credentials"):
connector._get_basic_creds()
@patch("app.integrations.caldav_calendar.CalDAVClient")
def test_test_connection(self, mock_client_class, caldav_integration):
"""Test connection testing"""
mock_client = Mock()
mock_client.discover_calendars.return_value = [
CalDAVCalendar(
href="https://mail.example.com/dav/Calendar/", name="My Calendar"
)
]
mock_client.fetch_events.return_value = []
mock_client_class.return_value = mock_client
credentials = IntegrationCredential.query.filter_by(
integration_id=caldav_integration.id
).first()
connector = CalDAVCalendarConnector(caldav_integration, credentials)
result = connector.test_connection()
assert result["success"] is True
assert "Connected to CalDAV" in result["message"]
assert len(result["calendars"]) == 1
@patch("app.integrations.caldav_calendar.CalDAVClient")
def test_sync_data_imports_events(
self, mock_client_class, db_session, caldav_integration, test_project
):
"""Test syncing imports calendar events as time entries"""
# Mock calendar event
now_utc = datetime.now(timezone.utc)
event_data = {
"uid": "test-event-123",
"summary": "Meeting with Test Project",
"description": "Important meeting",
"start": now_utc - timedelta(hours=1),
"end": now_utc,
"href": "https://mail.example.com/dav/Calendar/test-event-123.ics",
}
mock_client = Mock()
mock_client.fetch_events.return_value = [event_data]
mock_client_class.return_value = mock_client
credentials = IntegrationCredential.query.filter_by(
integration_id=caldav_integration.id
).first()
connector = CalDAVCalendarConnector(caldav_integration, credentials)
result = connector.sync_data()
assert result["success"] is True
assert result["imported"] == 1
assert result["skipped"] == 0
# The connector was changed to write CalendarEvent records (not
# TimeEntry) and to track imports via a [CalDAV: <uid>] marker in
# the description, not an IntegrationExternalEventLink row.
calendar_event = CalendarEvent.query.filter_by(
user_id=caldav_integration.user_id
).first()
assert calendar_event is not None
assert "Meeting with Test Project" in calendar_event.title
assert "[CalDAV: test-event-123]" in (calendar_event.description or "")
@patch("app.integrations.caldav_calendar.CalDAVClient")
def test_sync_data_skips_duplicates(
self, mock_client_class, db_session, caldav_integration, test_project
):
"""Test sync skips already imported events"""
# Create existing link
time_entry = TimeEntry(
user_id=caldav_integration.user_id,
project_id=test_project.id,
start_time=datetime.now(),
end_time=datetime.now() + timedelta(hours=1),
notes="Existing entry",
)
db_session.add(time_entry)
db_session.flush()
link = IntegrationExternalEventLink(
integration_id=caldav_integration.id,
time_entry_id=time_entry.id,
external_uid="test-event-123",
)
db_session.add(link)
db_session.commit()
# Mock same event
now_utc = datetime.now(timezone.utc)
event_data = {
"uid": "test-event-123",
"summary": "Meeting",
"description": "",
"start": now_utc - timedelta(hours=1),
"end": now_utc,
"href": "https://mail.example.com/dav/Calendar/test-event-123.ics",
}
mock_client = Mock()
mock_client.fetch_events.return_value = [event_data]
mock_client_class.return_value = mock_client
credentials = IntegrationCredential.query.filter_by(
integration_id=caldav_integration.id
).first()
connector = CalDAVCalendarConnector(caldav_integration, credentials)
result = connector.sync_data()
assert result["success"] is True
assert result["imported"] == 0
assert result["skipped"] == 1
# Verify no duplicate time entry
entries = TimeEntry.query.filter_by(user_id=caldav_integration.user_id).all()
assert len(entries) == 1
class TestCalDAVRoutes:
"""Test CalDAV routes"""
def test_caldav_setup_get(self, authenticated_client, test_user, test_project):
"""Test CalDAV setup page loads"""
response = authenticated_client.get(url_for("integrations.caldav_setup"))
assert response.status_code == 200
assert b"CalDAV Calendar Setup" in response.data
def test_caldav_setup_post(self, authenticated_client, test_user, test_project):
"""Test CalDAV setup form submission"""
response = authenticated_client.post(
url_for("integrations.caldav_setup"),
data={
"server_url": "https://mail.example.com/dav",
"username": "user@example.com",
"password": "testpass",
"calendar_url": "https://mail.example.com/dav/user@example.com/Calendar/",
"calendar_name": "My Calendar",
"default_project_id": str(test_project.id),
"lookback_days": "90",
"verify_ssl": "on",
},
follow_redirects=False,
)
# Should redirect to view integration
assert response.status_code in (200, 302)
# Verify integration was created
integration = Integration.query.filter_by(
provider="caldav_calendar", user_id=test_user.id
).first()
assert integration is not None
assert integration.config["server_url"] == "https://mail.example.com/dav"
assert integration.config["default_project_id"] == test_project.id
# Verify credentials were saved
credentials = IntegrationCredential.query.filter_by(
integration_id=integration.id
).first()
assert credentials is not None
assert credentials.access_token == "testpass"
assert credentials.extra_data["username"] == "user@example.com"
class TestIntegrationExternalEventLink:
"""Test external event link model"""
def test_link_creation(self, db_session, caldav_integration, test_project):
"""Test creating an external event link"""
time_entry = TimeEntry(
user_id=caldav_integration.user_id,
project_id=test_project.id,
start_time=datetime.now(),
end_time=datetime.now() + timedelta(hours=1),
notes="Test entry",
)
db_session.add(time_entry)
db_session.flush()
link = IntegrationExternalEventLink(
integration_id=caldav_integration.id,
time_entry_id=time_entry.id,
external_uid="test-uid-123",
external_href="https://example.com/event.ics",
)
db_session.add(link)
db_session.commit()
assert link.id is not None
assert link.external_uid == "test-uid-123"
assert link.integration_id == caldav_integration.id
assert link.time_entry_id == time_entry.id
def test_link_unique_constraint(self, db_session, caldav_integration, test_project):
"""Test unique constraint on integration_id + external_uid"""
time_entry = TimeEntry(
user_id=caldav_integration.user_id,
project_id=test_project.id,
start_time=datetime.now(),
end_time=datetime.now() + timedelta(hours=1),
notes="Test entry",
)
db_session.add(time_entry)
db_session.flush()
link1 = IntegrationExternalEventLink(
integration_id=caldav_integration.id,
time_entry_id=time_entry.id,
external_uid="test-uid-123",
)
db_session.add(link1)
db_session.commit()
# Try to create duplicate
link2 = IntegrationExternalEventLink(
integration_id=caldav_integration.id,
time_entry_id=time_entry.id,
external_uid="test-uid-123",
)
db_session.add(link2)
with pytest.raises(Exception): # Should raise IntegrityError
db_session.commit()