From 3e11794d023dc7b0f1448ce2aa18f5cedfd8a345 Mon Sep 17 00:00:00 2001
From: Sylvain Zimmer
Date: Sat, 21 Feb 2026 00:49:44 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=92=9A(repo)=20fix=20CI=20and=20general?=
=?UTF-8?q?=20cleanup=20(#12)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/calendars-frontend.yml | 56 +-----------
.github/workflows/calendars.yml | 91 +++++--------------
.github/workflows/crowdin_download.yml | 4 +-
.github/workflows/crowdin_upload.yml | 2 +-
.github/workflows/docker-hub.yml | 56 +++++-------
.../front-dependencies-installation.yml | 2 +-
CHANGELOG.md | 8 +-
README.md | 8 +-
src/backend/Dockerfile | 4 -
src/backend/calendars/settings.py | 3 +-
src/backend/core/api/viewsets_rsvp.py | 5 +-
src/backend/core/authentication/backends.py | 1 -
src/backend/core/services/caldav_service.py | 16 ++--
.../core/services/translation_service.py | 9 +-
.../tests/authentication/test_backends.py | 4 +-
src/backend/core/tests/conftest.py | 3 +-
src/backend/core/tests/test_api_users.py | 6 +-
src/backend/core/tests/test_caldav_proxy.py | 34 ++++---
.../core/tests/test_caldav_scheduling.py | 8 +-
src/backend/core/tests/test_caldav_service.py | 8 +-
.../tests/test_calendar_invitation_service.py | 2 +
src/backend/core/tests/test_ical_export.py | 1 -
src/backend/core/tests/test_models_users.py | 2 +-
src/backend/core/tests/test_rsvp.py | 2 +
.../core/tests/test_translation_service.py | 7 +-
src/backend/gunicorn.conf.py | 4 +-
src/frontend/Dockerfile.nginx | 20 ++++
.../scheduler/hooks/useSchedulerInit.ts | 2 +-
.../calendar/services/dav/CalDavService.ts | 2 +-
.../docker/files/usr/local/bin/entrypoint | 4 +
30 files changed, 152 insertions(+), 222 deletions(-)
create mode 100644 src/frontend/Dockerfile.nginx
create mode 100755 src/frontend/docker/files/usr/local/bin/entrypoint
diff --git a/.github/workflows/calendars-frontend.yml b/.github/workflows/calendars-frontend.yml
index 910f660..8ed69cc 100644
--- a/.github/workflows/calendars-frontend.yml
+++ b/.github/workflows/calendars-frontend.yml
@@ -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
diff --git a/.github/workflows/calendars.yml b/.github/workflows/calendars.yml
index 3b1d207..11c9b23 100644
--- a/.github/workflows/calendars.yml
+++ b/.github/workflows/calendars.yml
@@ -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: |
@@ -76,7 +51,7 @@ jobs:
uses: astral-sh/setup-uv@v6
- name: Install the project
run: uv sync --locked --all-extras
-
+
- name: Check code formatting with ruff
run: uv run ruff format . --diff
- name: Lint code with ruff
@@ -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
diff --git a/.github/workflows/crowdin_download.yml b/.github/workflows/crowdin_download.yml
index 3689804..a89ef78 100644
--- a/.github/workflows/crowdin_download.yml
+++ b/.github/workflows/crowdin_download.yml
@@ -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
diff --git a/.github/workflows/crowdin_upload.yml b/.github/workflows/crowdin_upload.yml
index cc6194d..87516d9 100644
--- a/.github/workflows/crowdin_upload.yml
+++ b/.github/workflows/crowdin_upload.yml
@@ -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
diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml
index 120d937..7dba459 100644
--- a/.github/workflows/docker-hub.yml
+++ b/.github/workflows/docker-hub.yml
@@ -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 }}"
diff --git a/.github/workflows/front-dependencies-installation.yml b/.github/workflows/front-dependencies-installation.yml
index baecac9..642f4d3 100644
--- a/.github/workflows/front-dependencies-installation.yml
+++ b/.github/workflows/front-dependencies-installation.yml
@@ -5,7 +5,7 @@ on:
inputs:
node_version:
required: false
- default: '20.x'
+ default: '22.x'
type: string
jobs:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 236c2c5..d6cb3fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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).
diff --git a/README.md b/README.md
index d51a531..0a40917 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
-
+
Chat on Matrix
-
Documentation
-
Getting started
- -
+ -
Reach out
@@ -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!
diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile
index 059c691..66b8ee9 100644
--- a/src/backend/Dockerfile
+++ b/src/backend/Dockerfile
@@ -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}
diff --git a/src/backend/calendars/settings.py b/src/backend/calendars/settings.py
index d0f8010..2989718 100755
--- a/src/backend/calendars/settings.py
+++ b/src/backend/calendars/settings.py
@@ -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
)
diff --git a/src/backend/core/api/viewsets_rsvp.py b/src/backend/core/api/viewsets_rsvp.py
index 42a81b2..7a6731e 100644
--- a/src/backend/core/api/viewsets_rsvp.py
+++ b/src/backend/core/api/viewsets_rsvp.py
@@ -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", "")
diff --git a/src/backend/core/authentication/backends.py b/src/backend/core/authentication/backends.py
index 5c38894..a8715de 100644
--- a/src/backend/core/authentication/backends.py
+++ b/src/backend/core/authentication/backends.py
@@ -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__)
diff --git a/src/backend/core/services/caldav_service.py b/src/backend/core/services/caldav_service.py
index 7c1d6b2..d170528 100644
--- a/src/backend/core/services/caldav_service.py
+++ b/src/backend/core/services/caldav_service.py
@@ -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(
diff --git a/src/backend/core/services/translation_service.py b/src/backend/core/services/translation_service.py
index b6fe54a..ddee4c2 100644
--- a/src/backend/core/services/translation_service.py
+++ b/src/backend/core/services/translation_service.py
@@ -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:
diff --git a/src/backend/core/tests/authentication/test_backends.py b/src/backend/core/tests/authentication/test_backends.py
index d5f5da0..7d45da5 100644
--- a/src/backend/core/tests/authentication/test_backends.py
+++ b/src/backend/core/tests/authentication/test_backends.py
@@ -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",
diff --git a/src/backend/core/tests/conftest.py b/src/backend/core/tests/conftest.py
index 77f8bdd..17d0bef 100644
--- a/src/backend/core/tests/conftest.py
+++ b/src/backend/core/tests/conftest.py
@@ -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.
diff --git a/src/backend/core/tests/test_api_users.py b/src/backend/core/tests/test_api_users.py
index 94fdd35..642c663 100644
--- a/src/backend/core/tests/test_api_users.py
+++ b/src/backend/core/tests/test_api_users.py
@@ -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
diff --git a/src/backend/core/tests/test_caldav_proxy.py b/src/backend/core/tests/test_caldav_proxy.py
index 0da1d59..fb6a748 100644
--- a/src/backend/core/tests/test_caldav_proxy.py
+++ b/src/backend/core/tests/test_caldav_proxy.py
@@ -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 = ''
+ propfind_body = (
+ ''
+ ''
+ )
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
diff --git a/src/backend/core/tests/test_caldav_scheduling.py b/src/backend/core/tests/test_caldav_scheduling.py
index 2d0708c..8767981 100644
--- a/src/backend/core/tests/test_caldav_scheduling.py
+++ b/src/backend/core/tests/test_caldav_scheduling.py
@@ -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
diff --git a/src/backend/core/tests/test_caldav_service.py b/src/backend/core/tests/test_caldav_service.py
index 2e9d186..b83f20c 100644
--- a/src/backend/core/tests/test_caldav_service.py
+++ b/src/backend/core/tests/test_caldav_service.py
@@ -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)
diff --git a/src/backend/core/tests/test_calendar_invitation_service.py b/src/backend/core/tests/test_calendar_invitation_service.py
index dac8779..2e5412b 100644
--- a/src/backend/core/tests/test_calendar_invitation_service.py
+++ b/src/backend/core/tests/test_calendar_invitation_service.py
@@ -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
diff --git a/src/backend/core/tests/test_ical_export.py b/src/backend/core/tests/test_ical_export.py
index df41cd1..4f92fae 100644
--- a/src/backend/core/tests/test_ical_export.py
+++ b/src/backend/core/tests/test_ical_export.py
@@ -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
diff --git a/src/backend/core/tests/test_models_users.py b/src/backend/core/tests/test_models_users.py
index 1bba4e2..edea5bb 100644
--- a/src/backend/core/tests/test_models_users.py
+++ b/src/backend/core/tests/test_models_users.py
@@ -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
diff --git a/src/backend/core/tests/test_rsvp.py b/src/backend/core/tests/test_rsvp.py
index 0f5af23..d5598e3 100644
--- a/src/backend/core/tests/test_rsvp.py
+++ b/src/backend/core/tests/test_rsvp.py
@@ -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
diff --git a/src/backend/core/tests/test_translation_service.py b/src/backend/core/tests/test_translation_service.py
index 0264069..96f0812 100644
--- a/src/backend/core/tests/test_translation_service.py
+++ b/src/backend/core/tests/test_translation_service.py
@@ -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
diff --git a/src/backend/gunicorn.conf.py b/src/backend/gunicorn.conf.py
index dd33428..cb6f214 100644
--- a/src/backend/gunicorn.conf.py
+++ b/src/backend/gunicorn.conf.py
@@ -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"
diff --git a/src/frontend/Dockerfile.nginx b/src/frontend/Dockerfile.nginx
new file mode 100644
index 0000000..88830fb
--- /dev/null
+++ b/src/frontend/Dockerfile.nginx
@@ -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;"]
diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts
index df33568..21c676b 100644
--- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts
+++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts
@@ -196,7 +196,7 @@ export const useSchedulerInit = ({
}
// Only fetch source events if we actually need recurrence rules
- let sourceRulesByUid = new Map();
+ const sourceRulesByUid = new Map();
if (uidsNeedingRules.size > 0) {
const sourceResult = await caldavService.fetchEvents(
calendar.url, { timeRange, expand: false }
diff --git a/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts b/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts
index bf1870f..4b19469 100644
--- a/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts
+++ b/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts
@@ -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' }
diff --git a/src/frontend/docker/files/usr/local/bin/entrypoint b/src/frontend/docker/files/usr/local/bin/entrypoint
new file mode 100755
index 0000000..8959ebc
--- /dev/null
+++ b/src/frontend/docker/files/usr/local/bin/entrypoint
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -e
+
+exec "$@"