Skip to content

Commit fa48353

Browse files
authored
Merge pull request #742 from Nickatak/refactor/test-shape
refactor: Split monolithic test file into per-resource files
2 parents 935fc41 + 71df779 commit fa48353

11 files changed

Lines changed: 364 additions & 171 deletions

backend/ctj_api/tests/common.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Shared factory helpers for `ctj_api` test files.
2+
3+
Each helper saves a model instance with sensible defaults that callers
4+
can override via keyword arguments. The helpers do not return shared
5+
state - each call creates a new row. Callers wire factories together
6+
explicitly when they need relationships (e.g. an `Opportunity` needs
7+
a `Project`, a `Role`, and a `created_by` user; the test's `setUp`
8+
creates the dependencies and passes them in).
9+
10+
Per-test-file `setUp` methods import only the helpers they need.
11+
"""
12+
13+
from ctj_api.models import (
14+
CommunityOfPractice,
15+
CustomUser,
16+
Opportunity,
17+
Project,
18+
Role,
19+
Skill,
20+
)
21+
22+
23+
def make_pm_user(
24+
*,
25+
username: str = "pm_user",
26+
email: str = "pm_user@example.com",
27+
password: str = "password123",
28+
people_depot_user_id: str = "pm_user_pd_id",
29+
) -> CustomUser:
30+
"""Create a project-manager user."""
31+
return CustomUser.objects.create_user(
32+
username=username,
33+
email=email,
34+
password=password,
35+
people_depot_user_id=people_depot_user_id,
36+
isProjectManager=True,
37+
)
38+
39+
40+
def make_regular_user(
41+
*,
42+
username: str = "regular_user",
43+
email: str = "regular_user@example.com",
44+
password: str = "password123",
45+
people_depot_user_id: str = "regular_user_pd_id",
46+
) -> CustomUser:
47+
"""Create a non-PM user."""
48+
return CustomUser.objects.create_user(
49+
username=username,
50+
email=email,
51+
password=password,
52+
people_depot_user_id=people_depot_user_id,
53+
isProjectManager=False,
54+
)
55+
56+
57+
def make_cop(
58+
*,
59+
practice_area: str = "engineering",
60+
description: str = "Engineering CoP",
61+
) -> CommunityOfPractice:
62+
"""Create a Community of Practice row."""
63+
return CommunityOfPractice.objects.create(
64+
practice_area=practice_area,
65+
description=description,
66+
)
67+
68+
69+
def make_role(*, title: str = "Developer", cop: CommunityOfPractice) -> Role:
70+
"""Create a Role row anchored to the given CoP."""
71+
return Role.objects.create(title=title, community_of_practice=cop)
72+
73+
74+
def make_skill(*, name: str = "Python") -> Skill:
75+
"""Create a Skill row."""
76+
return Skill.objects.create(name=name)
77+
78+
79+
def make_project(
80+
*,
81+
name: str = "Civic Tech Jobs",
82+
people_depot_project_id: str = "1234-abcd",
83+
) -> Project:
84+
"""Create a Project row."""
85+
return Project.objects.create(
86+
name=name,
87+
people_depot_project_id=people_depot_project_id,
88+
)
89+
90+
91+
def make_opportunity(
92+
*,
93+
project: Project,
94+
role: Role,
95+
created_by: CustomUser,
96+
body: str = "Test opportunity",
97+
min_experience_required: str = "junior",
98+
min_hours_required: int = 10,
99+
work_environment: str = "remote",
100+
status: str = "open",
101+
) -> Opportunity:
102+
"""Create an Opportunity row.
103+
104+
Caller supplies the FK rows (project, role, created_by) explicitly
105+
because tests typically want to assert against a specific PM user
106+
or project; default-constructing them inside the helper would
107+
obscure those references.
108+
"""
109+
return Opportunity.objects.create(
110+
project=project,
111+
role=role,
112+
body=body,
113+
min_experience_required=min_experience_required,
114+
min_hours_required=min_hours_required,
115+
work_environment=work_environment,
116+
status=status,
117+
created_by=created_by,
118+
)

backend/ctj_api/tests/test_api_endpoints.py

Lines changed: 0 additions & 168 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Tests for `/api/communityOfPractice/` (read-only catalog)."""
2+
3+
from rest_framework import status
4+
from rest_framework.test import APITestCase
5+
6+
from ctj_api.tests.common import make_cop
7+
8+
9+
class CommunityOfPracticeReadTests(APITestCase):
10+
"""Read-only catalog endpoints for `CommunityOfPractice`."""
11+
12+
def setUp(self):
13+
self.cop = make_cop()
14+
15+
def test_list_returns_all_cops(self):
16+
"""GET on the CoP list returns 200 with all rows serialized."""
17+
response = self.client.get("/api/communityOfPractice/")
18+
self.assertEqual(response.status_code, status.HTTP_200_OK)
19+
self.assertEqual(len(response.data), 1)
20+
self.assertEqual(response.data[0]["practice_area"], "engineering")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Tests for `GET /api/healthcheck`."""
2+
3+
from rest_framework import status
4+
from rest_framework.test import APITestCase
5+
6+
7+
class HealthcheckTests(APITestCase):
8+
"""Liveness probe at `/api/healthcheck`."""
9+
10+
def test_healthcheck_returns_uptime(self):
11+
"""Healthcheck returns 200 with an `uptime` key in the JSON body."""
12+
response = self.client.get("/api/healthcheck")
13+
self.assertEqual(response.status_code, status.HTTP_200_OK)
14+
self.assertIn("uptime", response.json())
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Tests for `/api/opportunities/` (the one full-CRUD ViewSet)."""
2+
3+
from rest_framework import status
4+
from rest_framework.test import APIClient, APITestCase
5+
6+
from ctj_api.tests.common import (
7+
make_cop,
8+
make_opportunity,
9+
make_pm_user,
10+
make_project,
11+
make_regular_user,
12+
make_role,
13+
)
14+
15+
16+
class OpportunityTests(APITestCase):
17+
"""`OpportunityViewSet` behavior at `/api/opportunities/`.
18+
19+
Reads are public; mutations are gated by `OpportunityPermission`.
20+
Only PMs can create; only the creator can update; any PM can
21+
delete. PATCH is currently always 403'd (flagged for fix in
22+
`ctj_api.permissions`).
23+
"""
24+
25+
def setUp(self):
26+
self.client = APIClient()
27+
self.pm_user = make_pm_user()
28+
self.regular_user = make_regular_user()
29+
self.cop = make_cop()
30+
self.role = make_role(cop=self.cop)
31+
self.project = make_project()
32+
self.opportunity = make_opportunity(
33+
project=self.project,
34+
role=self.role,
35+
created_by=self.pm_user,
36+
)
37+
38+
def test_anonymous_can_list_opportunities(self):
39+
"""Opportunity list is publicly readable (no auth required)."""
40+
response = self.client.get("/api/opportunities/")
41+
self.assertEqual(response.status_code, status.HTTP_200_OK)
42+
self.assertGreater(len(response.data), 0)
43+
44+
def test_regular_user_cannot_create_opportunity(self):
45+
"""Non-PM users get 403 on POST /api/opportunities/."""
46+
self.client.force_authenticate(user=self.regular_user)
47+
payload = {
48+
"project": str(self.project.id),
49+
"role": str(self.role.id),
50+
"body": "Unauthorized create attempt",
51+
"min_experience_required": "senior",
52+
"min_hours_required": 5,
53+
"work_environment": "remote",
54+
"status": "open",
55+
}
56+
response = self.client.post("/api/opportunities/", payload)
57+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

0 commit comments

Comments
 (0)