💚(repo) fix CI and general cleanup (#12)
This commit is contained in:
56
.github/workflows/calendars-frontend.yml
vendored
56
.github/workflows/calendars-frontend.yml
vendored
@@ -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
|
||||
|
||||
89
.github/workflows/calendars.yml
vendored
89
.github/workflows/calendars.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/crowdin_download.yml
vendored
4
.github/workflows/crowdin_download.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/crowdin_upload.yml
vendored
2
.github/workflows/crowdin_upload.yml
vendored
@@ -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
|
||||
|
||||
56
.github/workflows/docker-hub.yml
vendored
56
.github/workflows/docker-hub.yml
vendored
@@ -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 }}"
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: '20.x'
|
||||
default: '22.x'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
20
src/frontend/Dockerfile.nginx
Normal file
20
src/frontend/Dockerfile.nginx
Normal 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;"]
|
||||
@@ -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 }
|
||||
|
||||
@@ -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' }
|
||||
|
||||
4
src/frontend/docker/files/usr/local/bin/entrypoint
Executable file
4
src/frontend/docker/files/usr/local/bin/entrypoint
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
exec "$@"
|
||||
Reference in New Issue
Block a user