Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions BACKEND_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This documentation details standards for writing pytest based backend testing fi
## Contents

- [Standards](#standards-)
- [Quickstart: Local Pytest Setup](#quickstart-local-pytest-setup)

## Standards

Expand All @@ -29,4 +30,40 @@ This documentation details standards for writing pytest based backend testing fi
- End with the response code that is being tested if there is only one
- If there is only one API endpoint used in a testing file, it should be defined as a `SCREAMING_SNAKE_CASE` variable at the top of the file

## Quickstart: Local Pytest Setup

Use this flow to run backend pytest locally with Docker Postgres.

1. Start Docker Desktop.
2. From repo root, start the database container:

```bash
docker compose --env-file .env.dev up -d db
docker compose --env-file .env.dev ps db
```

3. Set host-side environment variables (PowerShell):

```powershell
$env:DJANGO_ENV="LOCAL_DEV"
$env:DATABASE_HOST="localhost"
$env:DATABASE_PORT="5432"
$env:DATABASE_NAME="activist"
$env:DATABASE_USER="postgres"
$env:DATABASE_PASSWORD="postgres"
```

4. Run backend tests:

```bash
pytest -v
# or run a specific subset
pytest backend/tests/test_models.py backend/tests/test_api.py -v --tb=short
```

Notes:

- Pytest uses `core.test_settings` via `backend/pyproject.toml`.
- `core.test_settings` defines explicit test DB defaults and can still be overridden by env vars.

<sub><a href="#top">Back to top.</a></sub>
17 changes: 10 additions & 7 deletions backend/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,17 @@
DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB

# MARK: API Settings
#
# Avoid running this setup during import of alternate settings modules
# (e.g. ``core.test_settings``), where ``core.settings`` is imported as a base.
if os.getenv("DJANGO_SETTINGS_MODULE") == "core.settings":
django.setup()

django.setup()
from rest_framework import viewsets # noqa: E402

from rest_framework import viewsets # noqa: E402
django_stubs_ext.monkeypatch(extra_classes=(viewsets.ModelViewSet,))

django_stubs_ext.monkeypatch(extra_classes=(viewsets.ModelViewSet,))
from rest_framework.settings import api_settings # noqa: E402

from rest_framework.settings import api_settings # noqa: E402

# Workaround #471 / monkeypatch() is overriding the REST_FRAMEWORK dict.
api_settings.reload()
# Workaround #471 / monkeypatch() is overriding the REST_FRAMEWORK dict.
api_settings.reload()
19 changes: 19 additions & 0 deletions backend/core/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Test settings for backend pytest runs."""

import os

from core.settings import * # noqa: F403

# Use explicit Postgres defaults for tests so database NAME/HOST/etc are never None.
# Values can still be overridden via environment variables.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DATABASE_NAME", "activist_test"),
"USER": os.getenv("DATABASE_USER", "postgres"),
"PASSWORD": os.getenv("DATABASE_PASSWORD", "postgres"),
"HOST": os.getenv("DATABASE_HOST", "localhost"),
"PORT": os.getenv("DATABASE_PORT", "5432"),
}
}
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ max-complexity = 10
line_length = 88

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "core.settings"
DJANGO_SETTINGS_MODULE = "core.test_settings"
python_files = "test_*.py"
addopts = "--nomigrations"

Expand Down
19 changes: 19 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest
from rest_framework.test import APIClient


@pytest.fixture
def api_client() -> APIClient:
return APIClient()


@pytest.mark.django_db
def test_get_events_list(api_client: APIClient) -> None:
response = api_client.get("/v1/events/events")
assert response.status_code == 200


@pytest.mark.django_db
def test_unauthenticated_cannot_create_event(api_client: APIClient) -> None:
response = api_client.post("/v1/events/events", {"name": "Test"}, format="json")
assert response.status_code == 401
18 changes: 18 additions & 0 deletions backend/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest

from authentication.factories import UserFactory
from events.models import Event


@pytest.mark.django_db
def test_event_creation() -> None:
user = UserFactory()
event = Event.objects.create(
created_by=user,
name="Climate March",
type="action",
location_type="online",
)

assert event.name == "Climate March"
assert str(event) == "Climate March"
45 changes: 45 additions & 0 deletions frontend/test-e2e/specs/all/smoke/smoke-user-flows.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { expect, test } from "~/test-e2e/global-fixtures";

test.describe("Smoke User Flows", { tag: ["@desktop", "@mobile", "@unauth"] }, () => {
test.use({ storageState: undefined });

test.beforeEach(async ({ context }) => {
await context.clearCookies();
});

test("Homepage loads and shows navigation", async ({ page }) => {
await page.goto("/home");

await expect(page).toHaveTitle(/activist/i);
await expect(page.getByRole("navigation").first()).toBeVisible();
});

test("User can open sign up page", async ({ page }) => {
await page.goto("/auth/sign-up");

await expect(page).toHaveURL(/\/auth\/sign-up/);
await expect(page.getByRole("form").first()).toBeVisible();
});

test("Sign in shows validation feedback on empty submit", async ({ page }) => {
await page.goto("/auth/sign-in");
await page.getByRole("button", { name: /sign in|login/i }).click();

const requiredText = page.getByText(/required/i).first();
const invalidFields = page.locator(
"input[aria-invalid='true'], textarea[aria-invalid='true']"
);

await expect(requiredText.or(invalidFields.first())).toBeVisible();
});

test("Events page displays heading and at least one event card", async ({
page,
}) => {
await page.goto("/events?view=list");

await expect(page.getByRole("heading", { name: /events/i })).toBeVisible();
await expect(page.locator("[data-testid='event-card']").first()).toBeVisible();
});
});
Loading