Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ jobs:
run: |
cd dashboard
make test-browser
- name: Run framework tests with OIDC configured
run: |
cd dashboard
make oidc-test-framework
- name: Run projects tests with OIDC configured
run: |
cd dashboard
make oidc-test-projects
- name: Run browser tests with OIDC configured
run: |
cd dashboard
make oidc-test-browser
- name: Check static files
run: |
cd dashboard
Expand Down
63 changes: 44 additions & 19 deletions dashboard/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ install: venv
$(PIP) install --upgrade pip
$(PIP) install -r requirements.txt

install-dev: install
$(PIP) install -r requirements-dev.txt

migrate: install
$(MANAGE) migrate

Expand All @@ -40,45 +37,73 @@ run: init
collectstatic: install
$(MANAGE) collectstatic --no-input

# For use in CI
collectstatic-clear: install
$(MANAGE) collectstatic --clear --no-input
makemigrations: install
$(MANAGE) makemigrations --no-input

test: test-framework test-projects test-browser

clean:
rm -rf $(VENV)
find . -type d -name "__pycache__" -exec rm -r {} +
find . -type f -name "*.pyc" -delete


# Extra targets for development and CI.

# For use in CI
collectstatic-check: collectstatic-clear
@changes=$$(git status --porcelain staticfiles | wc -l); \
if [ $$changes -gt 0 ]; then \
echo "Detected changes to static files. Run 'make collectstatic-clear' and commit the changes."; \
exit 1; \
fi

makemigrations: install
$(MANAGE) makemigrations --no-input
collectstatic-clear: install
$(MANAGE) collectstatic --clear --no-input

# For use in CI
makemigrations-check: makemigrations
@changes=$$(git status --porcelain */migrations | wc -l); \
if [ $$changes -gt 0 ]; then \
echo "Detected changes to migrations. Run 'make makemigrations' and commit the changes."; \
exit 1; \
fi

test: test-framework test-projects test-browser
install-dev: install
$(PIP) install -r requirements-dev.txt

# For use in CI
test-framework: install-dev
$(PYTEST) framework

# For use in CI
test-projects: install-dev
$(PYTEST) projects

# For use in CI
test-browser: install-dev
]test-browser: install-dev
$(VENV)/bin/python -m playwright install
$(PYTEST) test_browser.py

clean:
rm -rf $(VENV)
find . -type d -name "__pycache__" -exec rm -r {} +
find . -type f -name "*.pyc" -delete
oidc-test-framework: install-dev
DJANGO_OIDC_CLIENT_ID=fake_client_id \
DJANGO_OIDC_CLIENT_SECRET=fake_client_secret \
DJANGO_OIDC_AUTHORIZE_URL=https://example.com/oauth2/auth \
DJANGO_OIDC_ACCESS_TOKEN_URL=https://example.com/oauth2/token \
DJANGO_OIDC_USER_URL=https://example.com/userinfo \
DJANGO_OIDC_JWKS_URL=https://example.com/.well-known/jwks.json \
$(PYTEST) framework

oidc-test-projects: install-dev
DJANGO_OIDC_CLIENT_ID=fake_client_id \
DJANGO_OIDC_CLIENT_SECRET=fake_client_secret \
DJANGO_OIDC_AUTHORIZE_URL=https://example.com/oauth2/auth \
DJANGO_OIDC_ACCESS_TOKEN_URL=https://example.com/oauth2/token \
DJANGO_OIDC_USER_URL=https://example.com/userinfo \
DJANGO_OIDC_JWKS_URL=https://example.com/.well-known/jwks.json \
$(PYTEST) projects

oidc-test-browser: install-dev
$(VENV)/bin/python -m playwright install
DJANGO_OIDC_CLIENT_ID=fake_client_id \
DJANGO_OIDC_CLIENT_SECRET=fake_client_secret \
DJANGO_OIDC_AUTHORIZE_URL=https://example.com/oauth2/auth \
DJANGO_OIDC_ACCESS_TOKEN_URL=https://example.com/oauth2/token \
DJANGO_OIDC_USER_URL=https://example.com/userinfo \
DJANGO_OIDC_JWKS_URL=https://example.com/.well-known/jwks.json \
$(PYTEST) test_browser.py
111 changes: 111 additions & 0 deletions dashboard/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import sys
import types

import pytest

from django.contrib.auth.models import Permission, User
from django.http import HttpResponse


@pytest.fixture
def user_without_permissions(client):
user = User.objects.create_user(username="no_perm", password="password")
client.login(username="no_perm", password="password")
return user


@pytest.fixture
def user_can_change_commitment(client):
user = User.objects.create_user(username="change_commitment", password="password")
permission = Permission.objects.get(
codename="change_commitment",
content_type__app_label="projects",
)
user.user_permissions.add(permission)
client.login(username="change_commitment", password="password")
return user


@pytest.fixture
def user_can_change_projectobjectivecondition(client):
user = User.objects.create_user(
username="change_projectobjectivecondition", password="password"
)
permission = Permission.objects.get(
codename="change_projectobjectivecondition",
content_type__app_label="projects",
)
user.user_permissions.add(permission)
client.login(username="change_projectobjectivecondition", password="password")
return user


@pytest.fixture
def user_can_change_projectobjective(client):
user = User.objects.create_user(
username="change_projectobjective", password="password"
)
permission = Permission.objects.get(
codename="change_projectobjective",
content_type__app_label="projects",
)
user.user_permissions.add(permission)
client.login(username="change_projectobjective", password="password")
return user


@pytest.fixture
def user_can_change_workcycle(client):
user = User.objects.create_user(username="change_workcycle", password="password")
permission = Permission.objects.get(
codename="change_workcycle",
content_type__app_label="framework",
)
user.user_permissions.add(permission)
client.login(username="change_workcycle", password="password")
return user


@pytest.fixture
def user_can_view_workcycle(client):
user = User.objects.create_user(username="view_workcycle", password="password")
permission = Permission.objects.get(
codename="view_workcycle",
content_type__app_label="framework",
)
user.user_permissions.add(permission)
client.login(username="view_workcycle", password="password")
return user


@pytest.fixture
def user_is_staff(client):
user = User.objects.create_user(
username="staffmember", password="password", is_staff=True
)
client.login(username="staffmember", password="password")
return user


# Creat a "fake" mozilla_django_oidc.views so that tests will run,
# even if mozilla_django_oidc is not available at import time for
# tests.
Comment on lines +90 to +92
Copy link
Copy Markdown
Collaborator

@dwilding dwilding Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not keen on this approach. mozilla_django_oidc is listed in our application's requirements.txt, so we ought to be able to assume it's available in the venv at runtime and testing time.

Unless you very strongly object, I'm going to remove this shim.

I think it's important that we run tests with a known combination of dependencies, otherwise it reduces the effectiveness of the tests.

If possible, I recommend the Makefile for running tests : make test. But I appreciate that the Makefile isn't set up in an offline-friendly way at the moment. So alternatively, when you're online, this will make sure your venv has everything needed for offline work:

. .venv/bin/activate && pip install -r requirements.txt -r requirements-dev.txt

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. No objections.


if "mozilla_django_oidc" not in sys.modules:
oidc_module = types.ModuleType("mozilla_django_oidc")
oidc_views_module = types.ModuleType("mozilla_django_oidc.views")

class _DummyOIDCView:
@classmethod
def as_view(cls):
def _view(request, *args, **kwargs):
return HttpResponse("")

return _view

oidc_views_module.OIDCAuthenticationRequestView = _DummyOIDCView
oidc_views_module.OIDCAuthenticationCallbackView = _DummyOIDCView
oidc_views_module.OIDCLogoutView = _DummyOIDCView
oidc_module.views = oidc_views_module
sys.modules["mozilla_django_oidc"] = oidc_module
sys.modules["mozilla_django_oidc.views"] = oidc_views_module
39 changes: 9 additions & 30 deletions dashboard/framework/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,12 @@
import pytest

from django.urls import reverse
from django.contrib.auth.models import User, Permission
from django.contrib.messages import get_messages

from framework.models import WorkCycle, ObjectiveGroup, Objective, Level
from projects.models import Project, QI, ProjectObjective


@pytest.fixture
def user_can_change(client):
user = User.objects.create_user(username="user", password="password")
permission = Permission.objects.get(
codename="change_workcycle",
content_type__app_label="framework",
)
user.user_permissions.add(permission)
client.login(username="user", password="password")
return user


@pytest.fixture
def user_can_view(client):
user = User.objects.create_user(username="user", password="password")
permission = Permission.objects.get(
codename="view_workcycle",
content_type__app_label="framework",
)
user.user_permissions.add(permission)
client.login(username="user", password="password")
return user


@pytest.fixture
def work_cycle():
return WorkCycle.objects.create(
Expand Down Expand Up @@ -67,7 +42,7 @@ def project(objective):

@pytest.mark.django_db
def test_admin_apply_qis(
client, user_can_change, work_cycle, project, objective, level
client, user_can_change_workcycle, work_cycle, project, objective, level
):
"""Test that admin_apply_qis copies current QI values to workcycle QIs."""

Expand Down Expand Up @@ -109,7 +84,7 @@ def test_admin_apply_qis(

@pytest.mark.django_db
def test_admin_apply_qis_user_disallowed(
client, user_can_view, work_cycle, project, objective, level
client, user_can_view_workcycle, work_cycle, project, objective, level
):
"""Test that a user with framework.view_workcycle permission (only) can't copy QI values."""

Expand Down Expand Up @@ -140,7 +115,7 @@ def test_admin_apply_qis_user_disallowed(

@pytest.mark.django_db
def test_admin_apply_qis_with_multiple_projects(
client, user_can_change, work_cycle, objective, level
client, user_can_change_workcycle, work_cycle, objective, level
):
"""Test that admin_apply_qis updates QIs for multiple projects."""

Expand Down Expand Up @@ -182,7 +157,9 @@ def test_admin_apply_qis_with_multiple_projects(


@pytest.mark.django_db
def test_admin_apply_qis_shows_message(client, user_can_change, work_cycle, project):
def test_admin_apply_qis_shows_message(
client, user_can_change_workcycle, work_cycle, project
):
"""Test that admin_apply_qis displays an info message."""

url = reverse("framework:admin_apply_qis", args=[work_cycle.id])
Expand All @@ -195,7 +172,9 @@ def test_admin_apply_qis_shows_message(client, user_can_change, work_cycle, proj


@pytest.mark.django_db
def test_admin_apply_qis_with_no_projects(client, user_can_change, work_cycle):
def test_admin_apply_qis_with_no_projects(
client, user_can_change_workcycle, work_cycle
):
"""Test that admin_apply_qis works even when no projects exist."""

# Call the view with no projects
Expand Down
Loading
Loading