✅(back) add subscription and iCal tests
Add pytest tests for calendar subscription API endpoints and iCal export functionality. Covers token generation, validation, expiration and .ics file generation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
449
src/backend/core/tests/test_calendar_subscription_api.py
Normal file
449
src/backend/core/tests/test_calendar_subscription_api.py
Normal file
@@ -0,0 +1,449 @@
|
||||
"""Tests for calendar subscription token API."""
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_201_CREATED,
|
||||
HTTP_204_NO_CONTENT,
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
HTTP_403_FORBIDDEN,
|
||||
HTTP_404_NOT_FOUND,
|
||||
)
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.models import CalendarSubscriptionToken
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSubscriptionTokenViewSet:
|
||||
"""Tests for the new standalone SubscriptionTokenViewSet."""
|
||||
|
||||
def test_create_subscription_token(self):
|
||||
"""Test creating a subscription token for a calendar."""
|
||||
user = factories.UserFactory()
|
||||
caldav_path = f"/calendars/{user.email}/test-calendar-uuid/"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{
|
||||
"caldav_path": caldav_path,
|
||||
"calendar_name": "My Test Calendar",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
assert "token" in response.data
|
||||
assert "url" in response.data
|
||||
assert "/ical/" in response.data["url"]
|
||||
assert ".ics" in response.data["url"]
|
||||
assert response.data["caldav_path"] == caldav_path
|
||||
assert response.data["calendar_name"] == "My Test Calendar"
|
||||
|
||||
# Verify token was created in database
|
||||
assert CalendarSubscriptionToken.objects.filter(
|
||||
owner=user, caldav_path=caldav_path
|
||||
).exists()
|
||||
|
||||
def test_create_subscription_token_normalizes_path(self):
|
||||
"""Test that caldav_path is normalized to have leading/trailing slashes."""
|
||||
user = factories.UserFactory()
|
||||
caldav_path = f"calendars/{user.email}/test-uuid" # No leading/trailing slash
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
# Path should be normalized
|
||||
assert response.data["caldav_path"] == f"/calendars/{user.email}/test-uuid/"
|
||||
|
||||
def test_create_subscription_token_returns_existing(self):
|
||||
"""Test that creating a token when one exists returns the existing one."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
client.force_login(subscription.owner)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{
|
||||
"caldav_path": subscription.caldav_path,
|
||||
"calendar_name": "Updated Name",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.data["token"] == str(subscription.token)
|
||||
# Name should be updated
|
||||
subscription.refresh_from_db()
|
||||
assert subscription.calendar_name == "Updated Name"
|
||||
|
||||
def test_get_subscription_token_by_path(self):
|
||||
"""Test retrieving an existing subscription token by CalDAV path."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
client.force_login(subscription.owner)
|
||||
|
||||
url = reverse("subscription-tokens-by-path")
|
||||
response = client.get(url, {"caldav_path": subscription.caldav_path})
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.data["token"] == str(subscription.token)
|
||||
assert "url" in response.data
|
||||
|
||||
def test_get_subscription_token_not_found(self):
|
||||
"""Test retrieving token when none exists."""
|
||||
user = factories.UserFactory()
|
||||
caldav_path = f"/calendars/{user.email}/nonexistent/"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-by-path")
|
||||
response = client.get(url, {"caldav_path": caldav_path})
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
def test_get_subscription_token_missing_path(self):
|
||||
"""Test that missing caldav_path query param returns 400."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-by-path")
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_delete_subscription_token(self):
|
||||
"""Test revoking a subscription token."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
client.force_login(subscription.owner)
|
||||
|
||||
base_url = reverse("subscription-tokens-by-path")
|
||||
url = f"{base_url}?caldav_path={quote(subscription.caldav_path, safe='')}"
|
||||
response = client.delete(url)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
assert not CalendarSubscriptionToken.objects.filter(pk=subscription.pk).exists()
|
||||
|
||||
def test_delete_subscription_token_not_found(self):
|
||||
"""Test deleting token when none exists."""
|
||||
user = factories.UserFactory()
|
||||
caldav_path = f"/calendars/{user.email}/nonexistent/"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
base_url = reverse("subscription-tokens-by-path")
|
||||
url = f"{base_url}?caldav_path={quote(caldav_path, safe='')}"
|
||||
response = client.delete(url)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
def test_non_owner_cannot_create_token(self):
|
||||
"""Test that users cannot create tokens for other users' calendars."""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
caldav_path = f"/calendars/{other_user.email}/test-calendar/"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
def test_non_owner_cannot_get_token(self):
|
||||
"""Test that users cannot get tokens for other users' calendars."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
other_user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(other_user)
|
||||
|
||||
url = reverse("subscription-tokens-by-path")
|
||||
response = client.get(url, {"caldav_path": subscription.caldav_path})
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
def test_non_owner_cannot_delete_token(self):
|
||||
"""Test that users cannot delete tokens for other users' calendars."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
other_user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(other_user)
|
||||
|
||||
base_url = reverse("subscription-tokens-by-path")
|
||||
url = f"{base_url}?caldav_path={quote(subscription.caldav_path, safe='')}"
|
||||
response = client.delete(url)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
# Token should still exist
|
||||
assert CalendarSubscriptionToken.objects.filter(pk=subscription.pk).exists()
|
||||
|
||||
def test_unauthenticated_cannot_create_token(self):
|
||||
"""Test that unauthenticated users cannot create tokens."""
|
||||
user = factories.UserFactory()
|
||||
caldav_path = f"/calendars/{user.email}/test-calendar/"
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_unauthenticated_cannot_get_token(self):
|
||||
"""Test that unauthenticated users cannot get tokens."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("subscription-tokens-by-path")
|
||||
response = client.get(url, {"caldav_path": subscription.caldav_path})
|
||||
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_regenerate_token(self):
|
||||
"""Test regenerating a token by delete + create."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
old_token = subscription.token
|
||||
client = APIClient()
|
||||
client.force_login(subscription.owner)
|
||||
|
||||
base_by_path_url = reverse("subscription-tokens-by-path")
|
||||
by_path_url = f"{base_by_path_url}?caldav_path={quote(subscription.caldav_path, safe='')}"
|
||||
create_url = reverse("subscription-tokens-list")
|
||||
|
||||
# Delete old token
|
||||
response = client.delete(by_path_url)
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
|
||||
# Create new token
|
||||
response = client.post(
|
||||
create_url,
|
||||
{"caldav_path": subscription.caldav_path},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
assert response.data["token"] != str(old_token)
|
||||
|
||||
def test_unique_constraint_per_owner_calendar(self):
|
||||
"""Test that only one token can exist per owner+caldav_path."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
|
||||
# Try to create another token for the same path - should return existing
|
||||
client = APIClient()
|
||||
client.force_login(subscription.owner)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": subscription.caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Should return the existing token, not create a new one
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.data["token"] == str(subscription.token)
|
||||
assert (
|
||||
CalendarSubscriptionToken.objects.filter(owner=subscription.owner).count()
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPathInjectionProtection:
|
||||
"""
|
||||
Security tests for CalDAV path injection protection.
|
||||
|
||||
These tests verify that malicious paths are rejected to prevent:
|
||||
- Path traversal attacks (../)
|
||||
- Query parameter injection
|
||||
- Fragment injection
|
||||
- Access to other users' calendars via path manipulation
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"malicious_suffix",
|
||||
[
|
||||
# Path traversal attacks
|
||||
"../other-calendar/",
|
||||
"../../etc/passwd/",
|
||||
"..%2F..%2Fetc%2Fpasswd/", # URL-encoded traversal
|
||||
# Query parameter injection
|
||||
"uuid?export=true/",
|
||||
"uuid?admin=true/",
|
||||
# Fragment injection
|
||||
"uuid#malicious/",
|
||||
# Special characters that shouldn't be in calendar IDs
|
||||
"uuid;rm -rf/",
|
||||
"uuid|cat /etc/passwd/",
|
||||
"uuid$(whoami)/",
|
||||
"uuid`whoami`/",
|
||||
# Double slashes
|
||||
"uuid//",
|
||||
"/uuid/",
|
||||
# Spaces and other whitespace
|
||||
"uuid with spaces/",
|
||||
"uuid\ttab/",
|
||||
# Unicode tricks
|
||||
"uuid\u002e\u002e/", # Unicode dots
|
||||
],
|
||||
)
|
||||
def test_create_token_rejects_malicious_calendar_id(self, malicious_suffix):
|
||||
"""Test that malicious calendar IDs in paths are rejected."""
|
||||
user = factories.UserFactory()
|
||||
caldav_path = f"/calendars/{user.email}/{malicious_suffix}"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Should be rejected - either 403 (invalid format) or path doesn't normalize
|
||||
assert response.status_code == HTTP_403_FORBIDDEN, (
|
||||
f"Path '{caldav_path}' should be rejected but got {response.status_code}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"malicious_path",
|
||||
[
|
||||
# Completely wrong structure
|
||||
"/etc/passwd/",
|
||||
"/admin/calendars/user@test.com/uuid/",
|
||||
"/../calendars/user@test.com/uuid/",
|
||||
# Missing segments
|
||||
"/calendars/",
|
||||
"/calendars/user@test.com/",
|
||||
# Path traversal to access another user's calendar
|
||||
"/calendars/victim@test.com/../attacker@test.com/uuid/",
|
||||
],
|
||||
)
|
||||
def test_create_token_rejects_malformed_paths(self, malicious_path):
|
||||
"""Test that malformed CalDAV paths are rejected."""
|
||||
user = factories.UserFactory(email="attacker@test.com")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": malicious_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Should be rejected
|
||||
assert response.status_code == HTTP_403_FORBIDDEN, (
|
||||
f"Path '{malicious_path}' should be rejected but got {response.status_code}"
|
||||
)
|
||||
|
||||
def test_path_traversal_to_other_user_calendar_rejected(self):
|
||||
"""Test that path traversal to access another user's calendar is blocked."""
|
||||
attacker = factories.UserFactory(email="attacker@example.com")
|
||||
victim = factories.UserFactory(email="victim@example.com")
|
||||
client = APIClient()
|
||||
client.force_login(attacker)
|
||||
|
||||
# Try to access victim's calendar via path traversal
|
||||
malicious_paths = [
|
||||
f"/calendars/{attacker.email}/../{victim.email}/secret-calendar/",
|
||||
f"/calendars/{victim.email}/secret-calendar/", # Direct access
|
||||
]
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
for path in malicious_paths:
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": path},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == HTTP_403_FORBIDDEN, (
|
||||
f"Attacker should not access victim's calendar via '{path}'"
|
||||
)
|
||||
|
||||
def test_valid_uuid_path_accepted(self):
|
||||
"""Test that valid UUID-style calendar IDs are accepted."""
|
||||
user = factories.UserFactory()
|
||||
# Standard UUID format
|
||||
caldav_path = f"/calendars/{user.email}/550e8400-e29b-41d4-a716-446655440000/"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
|
||||
def test_valid_alphanumeric_path_accepted(self):
|
||||
"""Test that valid alphanumeric calendar IDs are accepted."""
|
||||
user = factories.UserFactory()
|
||||
# Alphanumeric with hyphens (allowed by regex)
|
||||
caldav_path = f"/calendars/{user.email}/my-calendar-2024/"
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("subscription-tokens-list")
|
||||
response = client.post(
|
||||
url,
|
||||
{"caldav_path": caldav_path},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_201_CREATED
|
||||
|
||||
def test_get_token_with_malicious_path_rejected(self):
|
||||
"""Test that GET requests with malicious paths are rejected."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
malicious_path = f"/calendars/{user.email}/../../../etc/passwd/"
|
||||
|
||||
url = reverse("subscription-tokens-by-path")
|
||||
response = client.get(url, {"caldav_path": malicious_path})
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
def test_delete_token_with_malicious_path_rejected(self):
|
||||
"""Test that DELETE requests with malicious paths are rejected."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
malicious_path = f"/calendars/{user.email}/../../../etc/passwd/"
|
||||
|
||||
base_url = reverse("subscription-tokens-by-path")
|
||||
url = f"{base_url}?caldav_path={quote(malicious_path, safe='')}"
|
||||
response = client.delete(url)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
227
src/backend/core/tests/test_ical_export.py
Normal file
227
src/backend/core/tests/test_ical_export.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""Tests for iCal export endpoint."""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_502_BAD_GATEWAY
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.models import CalendarSubscriptionToken
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestICalExport:
|
||||
"""Tests for ICalExportView."""
|
||||
|
||||
def test_export_with_valid_token_returns_ics(self):
|
||||
"""Test that a valid token returns iCal data."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
|
||||
# Mock CalDAV server response
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
ics_content = b"""BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//SabreDAV//SabreDAV//EN
|
||||
BEGIN:VEVENT
|
||||
UID:test-event-123
|
||||
DTSTART:20240101T100000Z
|
||||
DTEND:20240101T110000Z
|
||||
SUMMARY:Test Event
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=ics_content,
|
||||
status=HTTP_200_OK,
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response["Content-Type"] == "text/calendar; charset=utf-8"
|
||||
assert "BEGIN:VCALENDAR" in response.content.decode()
|
||||
assert response["Content-Disposition"] is not None
|
||||
assert ".ics" in response["Content-Disposition"]
|
||||
|
||||
def test_export_with_invalid_token_returns_404(self):
|
||||
"""Test that an invalid token returns 404."""
|
||||
client = APIClient()
|
||||
invalid_token = uuid.uuid4()
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": invalid_token})
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
def test_export_with_inactive_token_returns_404(self):
|
||||
"""Test that an inactive token returns 404."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory(is_active=False)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
def test_export_updates_last_accessed_at(self):
|
||||
"""Test that accessing the export updates last_accessed_at."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
assert subscription.last_accessed_at is None
|
||||
|
||||
client = APIClient()
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=b"BEGIN:VCALENDAR\nEND:VCALENDAR",
|
||||
status=HTTP_200_OK,
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
client.get(url)
|
||||
|
||||
subscription.refresh_from_db()
|
||||
assert subscription.last_accessed_at is not None
|
||||
|
||||
def test_export_does_not_require_authentication(self):
|
||||
"""Test that the endpoint is accessible without authentication."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
# Not logging in - should still work
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=b"BEGIN:VCALENDAR\nEND:VCALENDAR",
|
||||
status=HTTP_200_OK,
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
def test_export_sends_correct_headers_to_caldav(self):
|
||||
"""Test that the proxy sends correct authentication headers to CalDAV."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=b"BEGIN:VCALENDAR\nEND:VCALENDAR",
|
||||
status=HTTP_200_OK,
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
client.get(url)
|
||||
|
||||
# Verify headers sent to CalDAV
|
||||
assert len(rsps.calls) == 1
|
||||
request = rsps.calls[0].request
|
||||
assert request.headers["X-Forwarded-User"] == subscription.owner.email
|
||||
assert request.headers["X-Api-Key"] == settings.CALDAV_OUTBOUND_API_KEY
|
||||
|
||||
def test_export_handles_caldav_error(self):
|
||||
"""Test that CalDAV server errors are handled gracefully."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=b"Internal Server Error",
|
||||
status=500,
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == HTTP_502_BAD_GATEWAY
|
||||
|
||||
def test_export_sets_security_headers(self):
|
||||
"""Test that security headers are set correctly."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory()
|
||||
client = APIClient()
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=b"BEGIN:VCALENDAR\nEND:VCALENDAR",
|
||||
status=HTTP_200_OK,
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
response = client.get(url)
|
||||
|
||||
# Verify security headers
|
||||
assert response["Cache-Control"] == "no-store, private"
|
||||
assert response["Referrer-Policy"] == "no-referrer"
|
||||
|
||||
def test_export_uses_calendar_name_in_filename(self):
|
||||
"""Test that the export filename uses the calendar_name."""
|
||||
subscription = factories.CalendarSubscriptionTokenFactory(
|
||||
calendar_name="My Test Calendar"
|
||||
)
|
||||
client = APIClient()
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
caldav_url = settings.CALDAV_URL
|
||||
caldav_path = subscription.caldav_path.lstrip("/")
|
||||
target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export"
|
||||
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
target_url,
|
||||
body=b"BEGIN:VCALENDAR\nEND:VCALENDAR",
|
||||
status=HTTP_200_OK,
|
||||
content_type="text/calendar",
|
||||
)
|
||||
|
||||
url = reverse("ical-export", kwargs={"token": subscription.token})
|
||||
response = client.get(url)
|
||||
|
||||
assert "My Test Calendar.ics" in response["Content-Disposition"]
|
||||
Reference in New Issue
Block a user