Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
10 changes: 10 additions & 0 deletions .github/workflows/run-e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ jobs:
db_username: nest_user_e2e
env_file: .env.e2e-tests

- name: Seed e2e users
run: |
set -euo pipefail && set -a && source .env.e2e-tests && set +a
export DJANGO_DB_HOST=localhost
export DJANGO_REDIS_AUTH_ENABLED=False
export DJANGO_REDIS_HOST=localhost
export PYTHONPATH="${PWD}/src"
poetry run python manage.py e2e_seed_users
working-directory: backend

- name: Install frontend dependencies
uses: ./.github/actions/install-frontend-dependencies

Expand Down
Empty file.
37 changes: 37 additions & 0 deletions backend/src/apps/nest/api/internal/views/e2e_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""E2E-only login view."""

import json

from django.conf import settings
from django.contrib.auth import login
from django.http import Http404, HttpRequest, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from apps.nest.models import User


@csrf_exempt

Check failure on line 14 in backend/src/apps/nest/api/internal/views/e2e_login.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure disabling CSRF protection is safe here.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AaAgPTuXauFKiHjiKf5l&open=AaAgPTuXauFKiHjiKf5l&pullRequest=5454
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
Mr-Rahul-Paul marked this conversation as resolved.
Outdated
@require_POST
def e2e_login(request: HttpRequest) -> JsonResponse:
"""Log in a seeded e2e user and set the Django session cookie."""
if not settings.IS_E2E_ENVIRONMENT:
raise Http404

try:
payload = json.loads(request.body or b"{}")
except json.JSONDecodeError:
return JsonResponse({"message": "Invalid JSON.", "ok": False}, status=400)

username = (payload.get("username") or "").strip()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
if not username:
return JsonResponse({"message": "username is required.", "ok": False}, status=400)

try:
user = User.objects.get(username=username)
except User.DoesNotExist as exc:
raise Http404 from exc

login(request, user)

return JsonResponse({"ok": True, "username": username})
83 changes: 83 additions & 0 deletions backend/src/apps/nest/management/commands/e2e_seed_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Seed deterministic users for end-to-end tests."""

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone

from apps.core.utils import index
from apps.github.models.user import User as GithubUser
from apps.mentorship.models import Mentee, Mentor
from apps.nest.models import User as NestUser
from apps.owasp.models.entity_member import EntityMember
from apps.owasp.models.project import Project

E2E_USERS = (
("e2e-mentee", "mentee"),
("e2e-mentor", "mentor"),
("e2e-user", ""),
)


class Command(BaseCommand):
help = "Seed e2e test users."

def handle(self, *_args, **_options) -> None:
"""Create GitHub, Nest, and mentorship users for e2e tests."""
if not settings.IS_E2E_ENVIRONMENT:
error_message = "This command can only run in the e2e environment."
raise CommandError(error_message)

now = timezone.now()
with index.disable_indexing():
for login, role in E2E_USERS:
github_user, _ = GithubUser.objects.get_or_create(
login=login,
defaults={
"created_at": now,
"email": f"{login}@example.com",
"name": login,
"node_id": f"e2e_node_{login}",
"updated_at": now,
},
)
nest_user, _ = NestUser.objects.get_or_create(
username=login,
defaults={
"email": f"{login}@example.com",
"github_user": github_user,
},
)
if nest_user.github_user_id != github_user.id:
nest_user.github_user = github_user
nest_user.save(update_fields=["github_user"])
if role == "mentor":
Mentor.objects.get_or_create(
github_user=github_user,
defaults={"nest_user": nest_user},
)
elif role == "mentee":
Mentee.objects.get_or_create(
github_user=github_user,
defaults={"nest_user": nest_user},
)
elif login == "e2e-user":
project, _ = Project.objects.get_or_create(
key="www-project-e2e",
defaults={"name": "E2E Project"},
)
membership, _ = EntityMember.objects.get_or_create(
entity_id=project.id,
entity_type=ContentType.objects.get_for_model(Project),
member_name=login,
role=EntityMember.Role.LEADER,
defaults={
"is_active": True,
"is_reviewed": True,
"member": github_user,
},
)
membership.is_active = True
membership.is_reviewed = True
membership.member = github_user
membership.save(update_fields=["is_active", "is_reviewed", "member"])
5 changes: 5 additions & 0 deletions backend/src/settings/e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ class E2E(Base):
SECURE_PROXY_SSL_HEADER = None # type: ignore[assignment] # Django accepts None to disable.
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False

ALGOLIA = {
**Base.ALGOLIA,
"AUTO_INDEXING": False,
}
5 changes: 5 additions & 0 deletions backend/src/settings/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@
path("integrations/slack/interactivity/", slack_request_handler),
]

if settings.IS_E2E_ENVIRONMENT:
from apps.nest.api.internal.views.e2e_login import e2e_login

urlpatterns += [path("e2e/login/", e2e_login)]

if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
87 changes: 87 additions & 0 deletions backend/tests/unit/apps/nest/api/internal/views/e2e_login_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import json
from http import HTTPStatus
from unittest.mock import MagicMock, patch

import pytest
from django.http import Http404
from django.test import RequestFactory

from apps.nest.api.internal.views.e2e_login import e2e_login
from apps.nest.models import User


def _post(body: dict | str):
data = body if isinstance(body, str) else json.dumps(body)
return RequestFactory().post(
"/e2e/login/",
data=data,
content_type="application/json",
)


class TestE2ELoginView:
def test_returns_404_outside_e2e(self):
with (
patch(
"apps.nest.api.internal.views.e2e_login.settings.IS_E2E_ENVIRONMENT",
new=False,
),
pytest.raises(Http404),
):

Check warning on line 30 in backend/tests/unit/apps/nest/api/internal/views/e2e_login_test.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AaAgPT3HauFKiHjiKf5m&open=AaAgPT3HauFKiHjiKf5m&pullRequest=5454
e2e_login(_post({"username": "e2e-mentor"}))

def test_returns_400_for_invalid_json(self):
with patch(
"apps.nest.api.internal.views.e2e_login.settings.IS_E2E_ENVIRONMENT",
new=True,
):
response = e2e_login(_post("{"))

assert response.status_code == HTTPStatus.BAD_REQUEST
assert json.loads(response.content)["ok"] is False

def test_returns_400_without_username(self):
with patch(
"apps.nest.api.internal.views.e2e_login.settings.IS_E2E_ENVIRONMENT",
new=True,
):
response = e2e_login(_post({}))

assert response.status_code == HTTPStatus.BAD_REQUEST
assert json.loads(response.content)["ok"] is False

def test_returns_404_for_unknown_user(self):
with (
patch(
"apps.nest.api.internal.views.e2e_login.settings.IS_E2E_ENVIRONMENT",
new=True,
),
patch(
"apps.nest.api.internal.views.e2e_login.User.objects.get",
side_effect=User.DoesNotExist,
),
pytest.raises(Http404),
):

Check warning on line 64 in backend/tests/unit/apps/nest/api/internal/views/e2e_login_test.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AaAgPT3HauFKiHjiKf5n&open=AaAgPT3HauFKiHjiKf5n&pullRequest=5454
e2e_login(_post({"username": "missing"}))

def test_logs_in_user(self):
user = MagicMock()
request = _post({"username": "e2e-mentor"})

with (
patch(
"apps.nest.api.internal.views.e2e_login.settings.IS_E2E_ENVIRONMENT",
new=True,
),
patch(
"apps.nest.api.internal.views.e2e_login.User.objects.get",
return_value=user,
) as mock_get,
patch("apps.nest.api.internal.views.e2e_login.login") as mock_login,
):
response = e2e_login(request)

mock_get.assert_called_once_with(username="e2e-mentor")
mock_login.assert_called_once_with(request, user)
assert response.status_code == HTTPStatus.OK
assert json.loads(response.content) == {"ok": True, "username": "e2e-mentor"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from unittest.mock import MagicMock, patch

import pytest
from django.core.management import call_command
from django.core.management.base import CommandError

from apps.nest.management.commands.e2e_seed_users import E2E_USERS, Command


class TestE2ESeedUsersCommand:
def test_metadata(self):
assert Command.help == "Seed e2e test users."
assert E2E_USERS == (
("e2e-mentee", "mentee"),
("e2e-mentor", "mentor"),
("e2e-user", ""),
)

def test_requires_e2e_environment(self):
with (
patch(
"apps.nest.management.commands.e2e_seed_users.settings.IS_E2E_ENVIRONMENT",
new=False,
),
pytest.raises(CommandError, match="e2e environment"),
):
call_command("e2e_seed_users")

@patch("apps.nest.management.commands.e2e_seed_users.index.disable_indexing")
@patch("apps.nest.management.commands.e2e_seed_users.ContentType")
@patch("apps.nest.management.commands.e2e_seed_users.EntityMember")
@patch("apps.nest.management.commands.e2e_seed_users.Project")
@patch("apps.nest.management.commands.e2e_seed_users.Mentee")
@patch("apps.nest.management.commands.e2e_seed_users.Mentor")
@patch("apps.nest.management.commands.e2e_seed_users.NestUser")
@patch("apps.nest.management.commands.e2e_seed_users.GithubUser")
def test_creates_users(
self,
mock_github_user,
mock_nest_user,
mock_mentor,
mock_mentee,
mock_project,
mock_entity_member,
mock_content_type,
mock_disable_indexing,
):
github_user = MagicMock()
github_user.id = 10
nest_user = MagicMock()
nest_user.github_user_id = 10
project = MagicMock(id=1)
membership = MagicMock()
mock_github_user.objects.get_or_create.return_value = (github_user, True)
mock_nest_user.objects.get_or_create.return_value = (nest_user, True)
mock_project.objects.get_or_create.return_value = (project, True)
mock_entity_member.objects.get_or_create.return_value = (membership, True)

with patch(
"apps.nest.management.commands.e2e_seed_users.settings.IS_E2E_ENVIRONMENT",
new=True,
):
call_command("e2e_seed_users")

mock_disable_indexing.assert_called_once()
assert mock_github_user.objects.get_or_create.call_count == 3
assert mock_nest_user.objects.get_or_create.call_count == 3
mock_mentor.objects.get_or_create.assert_called_once_with(
github_user=github_user,
defaults={"nest_user": nest_user},
)
mock_mentee.objects.get_or_create.assert_called_once_with(
github_user=github_user,
defaults={"nest_user": nest_user},
)
mock_project.objects.get_or_create.assert_called_once_with(
key="www-project-e2e",
defaults={"name": "E2E Project"},
)
mock_entity_member.objects.get_or_create.assert_called_once_with(
entity_id=project.id,
entity_type=mock_content_type.objects.get_for_model.return_value,
member_name="e2e-user",
role=mock_entity_member.Role.LEADER,
defaults={
"is_active": True,
"is_reviewed": True,
"member": github_user,
},
)
membership.save.assert_called_once_with(
update_fields=["is_active", "is_reviewed", "member"]
)
3 changes: 3 additions & 0 deletions docker-compose/e2e/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
command: >
sh -c '
python manage.py migrate &&
if [ "$${E2E_SKIP_SEED:-0}" != "1" ]; then python manage.py e2e_seed_users; fi &&
gunicorn wsgi:application --bind 0.0.0.0:9000
'
container_name: e2e-nest-backend
Expand All @@ -15,6 +16,8 @@ services:
condition: service_healthy
cache:
condition: service_healthy
environment:
E2E_SKIP_SEED: ${E2E_SKIP_SEED:-0}
env_file: ../../backend/.env.e2e-tests
healthcheck:
interval: 10s
Expand Down
4 changes: 2 additions & 2 deletions docker/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ ARG OWASP_UID

RUN apk --no-cache update && \
apk --no-cache upgrade && \
apk --no-cache add make postgresql16-client=16.14-r0 && \
apk --no-cache add make postgresql16-client=16.15-r0 && \
rm -rf /var/cache/apk/* /var/tmp/* /tmp/* && \
addgroup -S -g ${OWASP_GID} owasp && \
adduser -S -h /home/owasp -u ${OWASP_UID} -G owasp owasp
Expand Down Expand Up @@ -118,7 +118,7 @@ ARG OWASP_UID

RUN apk --no-cache update && \
apk --no-cache upgrade && \
apk --no-cache add ffmpeg make postgresql16-client=16.14-r0 \
apk --no-cache add ffmpeg make postgresql16-client=16.15-r0 \
# WeasyPrint dependencies: https://doc.courtbouillon.org/weasyprint/stable/first_steps.html
so:libfontconfig.so.1 so:libgobject-2.0.so.0 so:libharfbuzz-subset.so.0 \
so:libharfbuzz.so.0 so:libpango-1.0.so.0 so:libpangoft2-1.0.so.0 ttf-freefont && \
Expand Down
2 changes: 1 addition & 1 deletion docker/backend/Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ RUN mkdir -p ${APK_CACHE_DIR} && \

RUN --mount=type=cache,target=${APK_CACHE_DIR} \
apk --no-cache update && \
apk --no-cache add postgresql16-client=16.14-r0 redis && \
apk --no-cache add postgresql16-client=16.15-r0 redis && \
Comment thread
Mr-Rahul-Paul marked this conversation as resolved.
addgroup -S -g ${OWASP_GID} owasp && \
adduser -S -h /home/owasp -u ${OWASP_UID} -G owasp owasp

Expand Down
Loading
Loading