💚(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

@@ -57,58 +57,4 @@ jobs:
- name: Run unit tests
run: |
cd src/frontend/apps/calendars
yarn test
test-e2e:
runs-on: ubuntu-latest
needs: install-front
strategy:
matrix:
browser:
- chromium
- webkit
- firefox
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
fail-on-cache-miss: true
- name: Install Playwright Browsers
run: |
cd src/frontend/apps/e2e
npx playwright install --with-deps ${{matrix.browser}}
- name: Start Docker services
run: |
make bootstrap-e2e
- name: Start frontend
run: |
cd src/frontend && yarn dev &
- name: Wait for Keycloak to be ready
run: |
timeout 30 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8083/)" != "302" ]]; do echo "Waiting for Keycloak..." && sleep 2; done' && echo "Keycloak is ready!"
- name: Run e2e tests
run: |
cd src/frontend/apps/e2e
yarn test --project=${{ matrix.browser }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: report-${{ matrix.browser }}
path: src/frontend/apps/e2e/report/
retention-days: 7
TZ=Europe/Paris yarn test

View File

@@ -9,31 +9,6 @@ on:
- "*"
jobs:
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: show
run: git log
- name: Enforce absence of print statements in code
if: always()
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/calendars.yml' | grep "print("
- name: Check absence of fixup commits
if: always()
run: |
! git log | grep 'fixup!'
- name: Install gitlint
if: always()
run: pip install --user requests gitlint
- name: Lint commit messages added to main
if: always()
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
check-changelog:
runs-on: ubuntu-latest
if: |
@@ -86,7 +61,6 @@ jobs:
test-back:
runs-on: ubuntu-latest
needs: build-mails
defaults:
run:
@@ -101,7 +75,6 @@ jobs:
POSTGRES_PASSWORD: pass
ports:
- 5432:5432
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
env:
@@ -115,11 +88,11 @@ jobs:
DB_USER: pgroot
DB_PASSWORD: pass
DB_PORT: 5432
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_ACCESS_KEY_ID: calendar
AWS_S3_SECRET_ACCESS_KEY: password
MEDIA_BASE_URL: http://localhost:8083
CALDAV_URL: http://localhost:80
CALDAV_OUTBOUND_API_KEY: test-outbound-key
CALDAV_INBOUND_API_KEY: test-inbound-key
CALDAV_CALLBACK_HOST: localhost
TRANSLATIONS_JSON_PATH: ${{ github.workspace }}/src/frontend/apps/calendars/src/features/i18n/translations.json
steps:
- name: Checkout repository
@@ -130,40 +103,27 @@ jobs:
sudo mkdir -p /data/media && \
sudo mkdir -p /data/static
- name: Restore the mail templates
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Start MinIO
- name: Build and start CalDAV server
working-directory: .
run: |
docker pull minio/minio
docker run -d --name minio \
-p 9000:9000 \
-e "MINIO_ACCESS_KEY=calendar" \
-e "MINIO_SECRET_KEY=password" \
-v /data/media:/data \
minio/minio server --console-address :9001 /data
docker build -t caldav-test docker/sabredav
docker run -d --name caldav-test \
--network host \
-e PGHOST=localhost \
-e PGPORT=5432 \
-e PGDATABASE=calendars \
-e PGUSER=pgroot \
-e PGPASSWORD=pass \
-e CALDAV_BASE_URI=/api/v1.0/caldav/ \
-e CALDAV_INBOUND_API_KEY=test-inbound-key \
-e CALDAV_OUTBOUND_API_KEY=test-outbound-key \
caldav-test \
sh -c "/usr/local/bin/init-database.sh && apache2-foreground"
# Tool to wait for a service to be ready
- name: Install Dockerize
- name: Wait for CalDAV to be ready
run: |
curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz | sudo tar -C /usr/local/bin -xzv
- name: Wait for MinIO to be ready
run: |
dockerize -wait tcp://localhost:9000 -timeout 10s
- name: Configure MinIO
run: |
MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
docker exec ${MINIO} sh -c \
"mc alias set calendar http://localhost:9000 calendar password && \
mc alias ls && \
mc mb calendar/calendar-media-storage && \
mc version enable calendar/calendar-media-storage"
timeout 30 bash -c 'until curl -s -o /dev/null http://localhost:80/; do sleep 1; done'
echo "CalDAV server is ready"
- name: "Set up Python"
uses: actions/setup-python@v6
@@ -177,8 +137,7 @@ jobs:
- name: Install gettext (required to compile messages) and MIME support
run: |
sudo apt-get update
sudo apt-get install -y gettext pandoc shared-mime-info
sudo wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
sudo apt-get install -y gettext pandoc shared-mime-info media-types
- name: Generate a MO file from strings extracted from the project
run: uv run python manage.py compilemessages

View File

@@ -10,7 +10,7 @@ jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
node_version: '22.x'
synchronize-with-crowdin:
runs-on: ubuntu-latest
@@ -23,8 +23,6 @@ jobs:
- name: Create empty source files
run: |
touch src/backend/locale/django.pot
mkdir -p src/frontend/packages/i18n/locales/impress/
touch src/frontend/packages/i18n/locales/impress/translations-crowdin.json
# crowdin workflow
- name: crowdin action
uses: crowdin/github-action@v2

View File

@@ -10,7 +10,7 @@ jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
node_version: '22.x'
synchronize-with-crowdin:
needs: install-front

View File

@@ -24,6 +24,7 @@ jobs:
uses: actions/checkout@v6
-
name: Set up QEMU
if: github.event_name != 'pull_request'
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
@@ -41,19 +42,13 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: 'docker.io/lasuite/calendars-backend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
context: ./src/backend
target: backend-production
platforms: linux/amd64,linux/arm64
platforms: ${{ github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
@@ -67,6 +62,7 @@ jobs:
uses: actions/checkout@v6
-
name: Set up QEMU
if: github.event_name != 'pull_request'
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
@@ -85,34 +81,28 @@ jobs:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: 'docker.io/lasuite/calendars-frontend:${{ github.sha }}'
-
name: Build and push
name: Build SSG assets (platform-independent, amd64 only)
uses: docker/build-push-action@v6
with:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
platforms: linux/amd64,linux/arm64
context: ./src/frontend
target: calendars-builder
platforms: linux/amd64
load: true
tags: calendars-builder:local
-
name: Extract SSG build output
run: |
docker create --name extract calendars-builder:local
docker cp extract:/home/frontend/apps/calendars/out ./src/frontend/out
docker rm extract
-
name: Build and push nginx image (multi-arch)
uses: docker/build-push-action@v6
with:
context: ./src/frontend
file: ./src/frontend/Dockerfile.nginx
platforms: ${{ github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
notify-argocd:
needs:
- build-and-push-frontend
- build-and-push-backend
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- uses: numerique-gouv/action-argocd-webhook-notification@main
id: notify
with:
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
argocd_webhook_secret: "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}"
argocd_url: "${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}"

View File

@@ -5,7 +5,7 @@ on:
inputs:
node_version:
required: false
default: '20.x'
default: '22.x'
type: string
jobs:

View File

@@ -1,6 +1,10 @@
All notable changes to this project will be documented in this file.
# Changelog
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
All notable changes to this project will be documented in this
file.
The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

View File

@@ -12,13 +12,13 @@
</p>
<p align="center">
<a href="https://matrix.to/#/#messages-official:matrix.org">
<a href="https://matrix.to/#/#calendars-official:matrix.org">
Chat on Matrix
</a> - <a href="/docs/">
Documentation
</a> - <a href="#getting-started-">
Getting started
</a> - <a href="contact@suite.anct.gouv.fr">
</a> - <a href="mailto:contact@suite.anct.gouv.fr">
Reach out
</a>
</p>
@@ -137,7 +137,7 @@ You can then login with sub `admin@example.com` and password `admin`.
## Feedback 🙋‍♂️🙋‍♀️
We'd love to hear your thoughts and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#messages-official:matrix.org).
We'd love to hear your thoughts and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#calendars-official:matrix.org).
## Contributing 🙌
@@ -152,4 +152,4 @@ While Calendars is a public driven initiative our licence choice is an invitatio
## Credits ❤️
Calendars is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/). We thank the contributors of all these projects for their awesome work!
Calendars is built on top of [Django REST Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/) and [SabreDAV](https://sabre.io/dav/). We thank the contributors of all these projects for their awesome work!

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 "$@"