mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-24 07:10:21 -05:00
ed934100a0
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
409 lines
14 KiB
Python
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()
|