💚(repo) fix CI and general cleanup (#12)

This commit is contained in:
Sylvain Zimmer
2026-02-21 00:49:44 +01:00
committed by GitHub
parent 4f4eccd9c8
commit 3e11794d02
30 changed files with 152 additions and 222 deletions

View File

@@ -143,10 +143,6 @@ ARG CALENDARS_STATIC_ROOT=/data/static
# Remove git, we don't need it in the production image
RUN apk del git
# Gunicorn
RUN mkdir -p /usr/local/etc/gunicorn
COPY docker/files/usr/local/etc/gunicorn/calendars.py /usr/local/etc/gunicorn/calendars.py
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}

View File

@@ -451,7 +451,8 @@ class Base(Configuration):
FRONTEND_FEEDBACK_BUTTON_SHOW = values.BooleanValue(
default=False, environ_name="FRONTEND_FEEDBACK_BUTTON_SHOW", environ_prefix=None
)
# For instance, you might want to bind this button to an external library to trigger survey instead of the build in feedback modal.
# Bind this button to an external library to trigger survey
# instead of the built-in feedback modal.
FRONTEND_FEEDBACK_BUTTON_IDLE = values.BooleanValue(
default=False, environ_name="FRONTEND_FEEDBACK_BUTTON_IDLE", environ_prefix=None
)

View File

@@ -2,6 +2,7 @@
import logging
import re
from datetime import timezone as dt_timezone
from django.core.signing import BadSignature, Signer
from django.shortcuts import render
@@ -78,7 +79,7 @@ def _is_event_past(icalendar_data):
if dt:
# Make timezone-aware if naive (assume UTC)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.replace(tzinfo=dt_timezone.utc)
return dt < timezone.now()
return False
@@ -88,7 +89,7 @@ def _is_event_past(icalendar_data):
class RSVPView(View):
"""Handle RSVP responses from invitation email links."""
def get(self, request): # noqa: PLR0911
def get(self, request): # noqa: PLR0911 # pylint: disable=too-many-return-statements
"""Process an RSVP response."""
token = request.GET.get("token", "")
action = request.GET.get("action", "")

View File

@@ -10,7 +10,6 @@ from lasuite.oidc_login.backends import (
OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend,
)
from core.authentication.exceptions import UserCannotAccessApp
from core.models import DuplicateEmailError
logger = logging.getLogger(__name__)

View File

@@ -67,7 +67,7 @@ class CalDAVHTTPClient:
url = f"{url}?{query}"
return url
def request( # noqa: PLR0913
def request( # noqa: PLR0913 # pylint: disable=too-many-arguments
self,
method: str,
email: str,
@@ -123,7 +123,7 @@ class CalDAVHTTPClient:
continue
logger.warning("Event UID %s not found in user %s calendars", uid, email)
return None, None
except Exception:
except Exception: # pylint: disable=broad-exception-caught
logger.exception("CalDAV error looking up event %s", uid)
return None, None
@@ -517,20 +517,18 @@ class CalendarService:
def create_default_calendar(self, user) -> str:
"""Create a default calendar for a user. Returns the caldav_path."""
from core.services.translation_service import TranslationService # noqa: PLC0415
from core.services.translation_service import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
TranslationService,
)
calendar_id = str(uuid4())
lang = TranslationService.resolve_language(email=user.email)
calendar_name = TranslationService.t(
"calendar.list.defaultCalendarName", lang
)
calendar_name = TranslationService.t("calendar.list.defaultCalendarName", lang)
return self.caldav.create_calendar(
user, calendar_name, calendar_id, color=settings.DEFAULT_CALENDAR_COLOR
)
def create_calendar(
self, user, name: str, color: str = ""
) -> str:
def create_calendar(self, user, name: str, color: str = "") -> str:
"""Create a new calendar for a user. Returns the caldav_path."""
calendar_id = str(uuid4())
return self.caldav.create_calendar(

View File

@@ -2,7 +2,6 @@
import json
import logging
import os
from datetime import datetime
from typing import Optional
@@ -67,7 +66,7 @@ class TranslationService:
return current if isinstance(current, str) else None
@classmethod
def t(cls, key: str, lang: str = "en", **kwargs) -> str:
def t(cls, key: str, lang: str = "en", **kwargs) -> str: # pylint: disable=invalid-name
"""Look up a translation key with interpolation.
Fallback chain: lang -> "en" -> key itself.
@@ -95,7 +94,7 @@ class TranslationService:
- Fallback: "fr".
"""
if request is not None:
from django.utils.translation import ( # noqa: PLC0415
from django.utils.translation import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
get_language,
)
@@ -105,7 +104,9 @@ class TranslationService:
if email:
try:
from core.models import User # noqa: PLC0415
from core.models import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel
User,
)
user = User.objects.filter(email=email).first()
if user and user.language:

View File

@@ -2,7 +2,6 @@
import random
import re
from unittest import mock
from django.core.exceptions import SuspiciousOperation
from django.test.utils import override_settings
@@ -14,7 +13,6 @@ from lasuite.oidc_login.backends import get_oidc_refresh_token
from core import models
from core.authentication.backends import OIDCAuthenticationBackend
from core.authentication.exceptions import UserCannotAccessApp
from core.factories import UserFactory
pytestmark = pytest.mark.django_db
@@ -500,7 +498,7 @@ def test_authentication_session_tokens(
status=200,
)
with django_assert_num_queries(5):
with django_assert_num_queries(6):
user = klass.authenticate(
request,
code="test-code",

View File

@@ -8,7 +8,6 @@ from django.db import connection
import pytest
import responses
from cryptography.fernet import Fernet
from core import factories
from core.tests.utils.urls import reload_urls
@@ -19,7 +18,7 @@ VIA = [USER, TEAM]
@pytest.fixture(autouse=True)
def truncate_caldav_tables(django_db_setup, django_db_blocker):
def truncate_caldav_tables(django_db_setup, django_db_blocker): # pylint: disable=unused-argument
"""Fixture to truncate CalDAV server tables at the start of each test.
CalDAV server tables are created by the CalDAV server container migrations, not Django.

View File

@@ -204,10 +204,10 @@ def test_api_users_list_query_email_matching():
client.force_login(user)
user1 = factories.UserFactory(email="alice.johnson@example.gouv.fr")
user2 = factories.UserFactory(email="alice.johnnson@example.gouv.fr")
user3 = factories.UserFactory(email="alice.kohlson@example.gouv.fr")
factories.UserFactory(email="alice.johnnson@example.gouv.fr")
factories.UserFactory(email="alice.kohlson@example.gouv.fr")
user4 = factories.UserFactory(email="alicia.johnnson@example.gouv.fr")
user5 = factories.UserFactory(email="alicia.johnnson@example.gov.uk")
factories.UserFactory(email="alicia.johnnson@example.gov.uk")
factories.UserFactory(email="alice.thomson@example.gouv.fr")
# Exact match returns only that user

View File

@@ -1,5 +1,7 @@
"""Tests for CalDAV proxy view."""
# pylint: disable=no-member
from xml.etree import ElementTree as ET
from django.conf import settings
@@ -47,7 +49,7 @@ class TestCalDAVProxy:
)
)
response = client.generic("PROPFIND", "/api/v1.0/caldav/")
client.generic("PROPFIND", "/api/v1.0/caldav/")
# Verify request was made to CalDAV server
assert len(responses.calls) == 1
@@ -84,7 +86,7 @@ class TestCalDAVProxy:
# Try to send a malicious X-Forwarded-User header as if we were another user
malicious_email = "attacker@example.com"
response = client.generic(
client.generic(
"PROPFIND",
"/api/v1.0/caldav/",
HTTP_X_FORWARDED_USER=malicious_email,
@@ -110,7 +112,7 @@ class TestCalDAVProxy:
reason="CalDAV server URL not configured - integration test requires real server",
)
def test_proxy_propfind_response_contains_prefixed_urls(self):
"""Integration test: PROPFIND responses from sabre/dav should contain URLs with proxy prefix.
"""PROPFIND responses should contain URLs with proxy prefix.
This test verifies that sabre/dav's BaseUriPlugin correctly uses X-Forwarded-Prefix
to generate URLs with the proxy prefix. It requires the CalDAV server to be running.
@@ -122,7 +124,10 @@ class TestCalDAVProxy:
# Make actual request to CalDAV server through proxy
# The server should use X-Forwarded-Prefix to generate URLs
propfind_body = '<?xml version="1.0"?><propfind xmlns="DAV:"><prop><resourcetype/></prop></propfind>'
propfind_body = (
'<?xml version="1.0"?>'
'<propfind xmlns="DAV:"><prop><resourcetype/></prop></propfind>'
)
response = client.generic(
"PROPFIND",
"/api/v1.0/caldav/",
@@ -131,7 +136,8 @@ class TestCalDAVProxy:
)
assert response.status_code == HTTP_207_MULTI_STATUS, (
f"Expected 207 Multi-Status, got {response.status_code}: {response.content.decode('utf-8', errors='ignore')}"
f"Expected 207 Multi-Status, got {response.status_code}: "
f"{response.content.decode('utf-8', errors='ignore')}"
)
# Parse the response XML
@@ -149,9 +155,10 @@ class TestCalDAVProxy:
href.startswith("/principals/") or href.startswith("/calendars/")
):
assert href.startswith("/api/v1.0/caldav/"), (
f"Expected URL to start with /api/v1.0/caldav/, got {href}. "
f"This indicates sabre/dav BaseUriPlugin is not using X-Forwarded-Prefix correctly. "
f"Full response: {response.content.decode('utf-8', errors='ignore')}"
f"Expected URL to start with /api/v1.0/caldav/, "
f"got {href}. BaseUriPlugin is not using "
f"X-Forwarded-Prefix correctly. Full response: "
f"{response.content.decode('utf-8', errors='ignore')}"
)
@responses.activate
@@ -284,9 +291,7 @@ class TestCalDAVProxy:
)
# Request a specific path
response = client.generic(
"PROPFIND", "/api/v1.0/caldav/principals/test@example.com/"
)
client.generic("PROPFIND", "/api/v1.0/caldav/principals/test@example.com/")
# Verify the request was made to the correct URL
assert len(responses.calls) == 1
@@ -333,22 +338,29 @@ class TestValidateCaldavProxyPath:
"""Tests for validate_caldav_proxy_path utility."""
def test_empty_path_is_valid(self):
"""Empty path should be valid."""
assert validate_caldav_proxy_path("") is True
def test_calendars_path_is_valid(self):
"""Standard calendars path should be valid."""
assert validate_caldav_proxy_path("calendars/user@ex.com/uuid/") is True
def test_principals_path_is_valid(self):
"""Standard principals path should be valid."""
assert validate_caldav_proxy_path("principals/user@ex.com/") is True
def test_traversal_is_rejected(self):
"""Directory traversal attempts should be rejected."""
assert validate_caldav_proxy_path("calendars/../../etc/passwd") is False
def test_null_byte_is_rejected(self):
"""Paths containing null bytes should be rejected."""
assert validate_caldav_proxy_path("calendars/user\x00/") is False
def test_unknown_prefix_is_rejected(self):
"""Paths without a known prefix should be rejected."""
assert validate_caldav_proxy_path("etc/passwd") is False
def test_leading_slash_calendars_is_valid(self):
"""Paths with leading slash should still be valid."""
assert validate_caldav_proxy_path("/calendars/user@ex.com/uuid/") is True

View File

@@ -2,6 +2,7 @@
import http.server
import logging
import os
import secrets
import socket
import threading
@@ -118,10 +119,9 @@ class TestCalDAVScheduling:
except OSError as e:
pytest.fail(f"Test server failed to start on port {port}: {e}")
# Use the named test container hostname
# The test container is created with --name backend-test in bin/pytest
# Docker Compose networking allows containers to reach each other by name
callback_url = f"http://backend-test:{port}/"
# In Docker Compose, use the container hostname; on bare host (CI), use localhost
callback_host = os.environ.get("CALDAV_CALLBACK_HOST", "backend-test")
callback_url = f"http://{callback_host}:{port}/"
try:
# Create an event with an attendee

View File

@@ -1,7 +1,5 @@
"""Tests for CalDAV service integration."""
from unittest.mock import Mock, patch
from django.conf import settings
import pytest
@@ -19,7 +17,7 @@ class TestCalDAVClient:
user = factories.UserFactory(email="test@example.com")
client = CalDAVClient()
dav_client = client._get_client(user)
dav_client = client._get_client(user) # pylint: disable=protected-access
# Verify the client is configured correctly
# Username and password should be None to prevent Basic auth
@@ -78,9 +76,7 @@ class TestCalDAVClient:
color = "#e74c3c"
# Create a calendar with a specific color
caldav_path = service.create_calendar(
user, name="Red Calendar", color=color
)
caldav_path = service.create_calendar(user, name="Red Calendar", color=color)
# Fetch the calendar info and verify the color was persisted
info = service.caldav.get_calendar_info(user, caldav_path)

View File

@@ -1,5 +1,7 @@
"""Tests for ICalendarParser and email template rendering."""
# pylint: disable=missing-function-docstring,protected-access
from django.template.loader import render_to_string
import pytest

View File

@@ -11,7 +11,6 @@ from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_502_BAD_
from rest_framework.test import APIClient
from core import factories
from core.models import CalendarSubscriptionToken
@pytest.mark.django_db

View File

@@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
import pytest
from core import factories, models
from core import factories
pytestmark = pytest.mark.django_db

View File

@@ -1,5 +1,7 @@
"""Tests for RSVP view and token generation."""
# pylint: disable=missing-function-docstring,protected-access
import re
from datetime import timedelta
from unittest.mock import patch

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from core.services.translation_service import TranslationService
class TestTranslationServiceLookup:
class TestTranslationServiceLookup: # pylint: disable=missing-function-docstring
"""Tests for key lookup and interpolation."""
def test_lookup_french_key(self):
@@ -43,7 +43,7 @@ class TestTranslationServiceLookup:
assert "passé" in value
class TestNormalizeLang:
class TestNormalizeLang: # pylint: disable=missing-function-docstring
"""Tests for language normalization."""
def test_normalize_fr_fr(self):
@@ -69,6 +69,7 @@ class TestFormatDate:
"""Tests for date formatting."""
def test_format_date_french(self):
"""Format date in French locale."""
dt = datetime(2026, 1, 23, 10, 0) # Friday
result = TranslationService.format_date(dt, "fr")
assert "vendredi" in result
@@ -77,6 +78,7 @@ class TestFormatDate:
assert "2026" in result
def test_format_date_english(self):
"""Format date in English locale."""
dt = datetime(2026, 1, 23, 10, 0) # Friday
result = TranslationService.format_date(dt, "en")
assert "Friday" in result
@@ -85,6 +87,7 @@ class TestFormatDate:
assert "2026" in result
def test_format_date_dutch(self):
"""Format date in Dutch locale."""
dt = datetime(2026, 1, 23, 10, 0) # Friday
result = TranslationService.format_date(dt, "nl")
assert "vrijdag" in result

View File

@@ -1,4 +1,6 @@
# Gunicorn-django settings
"""Gunicorn configuration for the Calendars backend."""
# pylint: disable=invalid-name
bind = ["0.0.0.0:8000"]
name = "calendars"
python_path = "/app"

View File

@@ -0,0 +1,20 @@
FROM nginxinc/nginx-unprivileged:alpine3.22
# Upgrade system packages to install security updates
USER root
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
COPY out /usr/share/nginx/html
COPY ./apps/calendars/conf/default.conf /etc/nginx/conf.d
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -196,7 +196,7 @@ export const useSchedulerInit = ({
}
// Only fetch source events if we actually need recurrence rules
let sourceRulesByUid = new Map<string, unknown>();
const sourceRulesByUid = new Map<string, unknown>();
if (uidsNeedingRules.size > 0) {
const sourceResult = await caldavService.fetchEvents(
calendar.url, { timeRange, expand: false }

View File

@@ -1126,7 +1126,7 @@ END:VCALENDAR`
scheduleOutboxUrl: string | null
scheduleInboxUrl: string | null
calendarUserAddressSet: string[]
rawResponse?: any
rawResponse?: unknown
}>> {
if (!this._account?.principalUrl) {
return { success: false, error: 'Not connected or principal URL not found' }

View File

@@ -0,0 +1,4 @@
#!/bin/sh
set -e
exec "$@"