Files
plane/docker-compose-test.yml
T
sriram veeraghanta 9f77ea5ebb fix: Add docker pytest runner and fix bugs the suite surfaced (#9138)
* chore(api): add docker compose test runner

Adds docker-compose-test.yml at the repo root that boots an isolated
postgres / valkey / rabbitmq / minio stack with health checks and tmpfs
data dirs, then runs pytest against it and exits. Includes a usage doc
under apps/api/tests/RUNNING_TESTS.md and a pointer in AGENTS.md.

Prereq: ./setup.sh (generates apps/api/.env).

Usage:
  docker compose -f docker-compose-test.yml up --build \
    --abort-on-container-exit --exit-code-from api-tests
  docker compose -f docker-compose-test.yml down -v

* fix(api): correct bugs surfaced by the pytest suite

Five small bugs caught by enabling the pytest contract suite end-to-end.
Each is independently justifiable:

- api/serializers/cycle.py + api/views/cycle.py: CycleCreateSerializer.validate
  required project_id in the request body, but the view only ever passes
  it through the URL kwarg. Cycle create/update via the public API was
  returning 400 "Project ID is required". Read project_id from
  serializer context (passed by the view) in addition to body/instance.

- app/views/api.py: ApiTokenEndpoint.get(pk) and patch(pk) did not filter
  out is_service=True tokens, so a user could read and modify service
  tokens through the user token endpoint. The list mode and delete
  already filter is_service=False; aligned the other two.

- bgtasks/work_item_link_task.py: validate_url_ip checked hostname before
  scheme, so file:///etc/passwd raised "No hostname found" instead of
  the documented "Only HTTP and HTTPS" error. Swapped the order so the
  scheme guard matches the docstring intent.

- utils/path_validator.py: get_allowed_hosts used `WEB_URL or APP_BASE_URL`
  so when both are configured to different hosts (the standard local
  setup: WEB_URL=:8000, APP_BASE_URL=:3000), only one was added to the
  allow-list. Redirects to APP_BASE_URL then had their next_path stripped
  because the host wasn't allowed. Include every configured base URL.

* chore(api): align pytest tests with current behavior, clear warnings

Test-side fixes paired with the product fixes in the previous commit, plus
deprecation cleanup that drops the test run from 104 warnings to 0.

Tests:
- tests/contract/api/test_cycles.py: project fixture sets cycle_view=True;
  the Project model defaults the flag to False, so cycle create/update
  always tripped "Cycles are not enabled for this project".
- tests/contract/app/test_authentication.py: next_path uses "/workspaces"
  (validate_next_path rejects values without a leading slash and returns
  empty, which dropped the path from the redirect URL).
- tests/unit/bg_tasks/test_copy_s3_objects.py: mocked sync_with_external_service
  now returns description_json; the task unconditionally writes the value
  back to the Issue, and Issue.description_json is NOT NULL on UPDATE.
- tests/unit/utils/test_url.py: three length-limit tests placed the URL at
  char 970+ on a single line, which contains_url truncates away as ReDoS
  defense (500-char per-line cap). Restructured to keep test intent intact
  while staying inside the per-line window.

Warning cleanup (104 → 0):
- settings/common.py: removed USE_L10N=True (deprecated in Django 4.0,
  removed in 5.0; default is True).
- celery.py, settings/local.py, settings/production.py: pythonjsonlogger
  moved jsonlogger → json; update the import / formatter path.
2026-05-26 01:21:37 +05:30

152 lines
4.7 KiB
YAML

# Docker Compose for running the API pytest suite in a contained environment.
#
# Prerequisite:
# ./setup.sh # creates apps/api/.env (and the other env files) from .env.example
#
# Usage:
# # Run the full suite (defaults to `pytest`):
# docker compose -f docker-compose-test.yml up --build --abort-on-container-exit --exit-code-from api-tests
#
# # Run a subset by overriding the command:
# docker compose -f docker-compose-test.yml run --rm --build api-tests pytest -m unit
# docker compose -f docker-compose-test.yml run --rm api-tests pytest plane/tests/unit -k "test_workspace"
#
# # Tear down (removes ephemeral volumes and the test network):
# docker compose -f docker-compose-test.yml down -v
#
# Notes:
# - Postgres / Valkey / RabbitMQ / MinIO start with health checks; the test
# runner only starts once each dependency is healthy.
# - Data dirs are tmpfs so every run begins from a clean state.
# - Env vars come from apps/api/.env (the same file the local stack uses); tests
# use plane.settings.test, which inherits from common and reads DATABASE_URL,
# REDIS_URL, RABBITMQ_* etc. from the environment.
services:
test-db:
image: postgres:15.7-alpine
networks:
- test_env
env_file:
- ./apps/api/.env
environment:
POSTGRES_USER: ${POSTGRES_USER:-plane}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
POSTGRES_DB: ${POSTGRES_DB:-plane}
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-plane} -d ${POSTGRES_DB:-plane}"]
interval: 5s
timeout: 5s
retries: 10
test-redis:
image: valkey/valkey:7.2.11-alpine
networks:
- test_env
tmpfs:
- /data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
test-mq:
image: rabbitmq:3.13.6-management-alpine
networks:
- test_env
env_file:
- ./apps/api/.env
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
tmpfs:
- /var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 10s
retries: 10
test-minio:
image: minio/minio
networks:
- test_env
env_file:
- ./apps/api/.env
environment:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
entrypoint: >
/bin/sh -c "
mkdir -p /export/${AWS_S3_BUCKET_NAME:-uploads} &&
minio server /export --console-address ':9090' &
sleep 3 &&
mc alias set local http://localhost:9000 ${AWS_ACCESS_KEY_ID:-access-key} ${AWS_SECRET_ACCESS_KEY:-secret-key} &&
mc mb local/${AWS_S3_BUCKET_NAME:-uploads} -p || true &&
tail -f /dev/null
"
tmpfs:
- /export
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 5s
retries: 10
api-tests:
build:
context: ./apps/api
dockerfile: Dockerfile.dev
args:
DOCKER_BUILDKIT: 1
networks:
- test_env
env_file:
- ./apps/api/.env
environment:
DJANGO_SETTINGS_MODULE: plane.settings.test
# Override service hostnames to point at the test-only services above.
POSTGRES_HOST: test-db
DATABASE_URL: postgresql://${POSTGRES_USER:-plane}:${POSTGRES_PASSWORD:-plane}@test-db:5432/${POSTGRES_DB:-plane}
REDIS_HOST: test-redis
REDIS_URL: redis://test-redis:6379/
RABBITMQ_HOST: test-mq
AWS_S3_ENDPOINT_URL: http://test-minio:9000
# Magic-link tests mock the celery delay but the view first checks that
# EMAIL_HOST is configured. Set a placeholder so the check passes.
EMAIL_HOST: test-smtp.invalid
volumes:
- ./apps/api:/code
working_dir: /code
depends_on:
test-db:
condition: service_healthy
test-redis:
condition: service_healthy
test-mq:
condition: service_healthy
test-minio:
condition: service_healthy
# Install test-only requirements (not in local.txt) then exec pytest.
# Any args passed via `docker compose run api-tests <args>` replace the default command.
entrypoint:
- /bin/sh
- -c
- |
set -e
pip install --no-cache-dir -r requirements/test.txt
# STATIC_ROOT must exist or Django's static middleware emits a
# UserWarning on every request (~100 of the warnings in test runs).
mkdir -p plane/static-assets/collected-static
exec "$@"
- --
command: ["pytest"]
networks:
test_env:
driver: bridge