|
c588255…
|
ragelink
|
1 |
import pytest |
|
c588255…
|
ragelink
|
2 |
from django.contrib.auth.models import User |
|
c588255…
|
ragelink
|
3 |
from django.test import Client |
|
c588255…
|
ragelink
|
4 |
|
|
c588255…
|
ragelink
|
5 |
from fossil.models import FossilRepository |
|
c588255…
|
ragelink
|
6 |
from fossil.ticket_reports import TicketReport |
|
c588255…
|
ragelink
|
7 |
from organization.models import Team |
|
c588255…
|
ragelink
|
8 |
from projects.models import ProjectTeam |
|
c588255…
|
ragelink
|
9 |
|
|
c588255…
|
ragelink
|
10 |
|
|
c588255…
|
ragelink
|
11 |
@pytest.fixture |
|
c588255…
|
ragelink
|
12 |
def fossil_repo_obj(sample_project): |
|
c588255…
|
ragelink
|
13 |
"""Return the auto-created FossilRepository for sample_project.""" |
|
c588255…
|
ragelink
|
14 |
return FossilRepository.objects.get(project=sample_project, deleted_at__isnull=True) |
|
c588255…
|
ragelink
|
15 |
|
|
c588255…
|
ragelink
|
16 |
|
|
c588255…
|
ragelink
|
17 |
@pytest.fixture |
|
c588255…
|
ragelink
|
18 |
def public_report(fossil_repo_obj, admin_user): |
|
c588255…
|
ragelink
|
19 |
return TicketReport.objects.create( |
|
c588255…
|
ragelink
|
20 |
repository=fossil_repo_obj, |
|
c588255…
|
ragelink
|
21 |
title="Open Tickets", |
|
c588255…
|
ragelink
|
22 |
description="All open tickets", |
|
c588255…
|
ragelink
|
23 |
sql_query="SELECT tkt_uuid, title, status FROM ticket WHERE status = 'Open'", |
|
c588255…
|
ragelink
|
24 |
is_public=True, |
|
c588255…
|
ragelink
|
25 |
created_by=admin_user, |
|
c588255…
|
ragelink
|
26 |
) |
|
c588255…
|
ragelink
|
27 |
|
|
c588255…
|
ragelink
|
28 |
|
|
c588255…
|
ragelink
|
29 |
@pytest.fixture |
|
c588255…
|
ragelink
|
30 |
def private_report(fossil_repo_obj, admin_user): |
|
c588255…
|
ragelink
|
31 |
return TicketReport.objects.create( |
|
c588255…
|
ragelink
|
32 |
repository=fossil_repo_obj, |
|
c588255…
|
ragelink
|
33 |
title="Internal Metrics", |
|
c588255…
|
ragelink
|
34 |
description="Admin-only report", |
|
c588255…
|
ragelink
|
35 |
sql_query="SELECT COUNT(*) as total FROM ticket", |
|
c588255…
|
ragelink
|
36 |
is_public=False, |
|
c588255…
|
ragelink
|
37 |
created_by=admin_user, |
|
c588255…
|
ragelink
|
38 |
) |
|
c588255…
|
ragelink
|
39 |
|
|
c588255…
|
ragelink
|
40 |
|
|
c588255…
|
ragelink
|
41 |
@pytest.fixture |
|
c588255…
|
ragelink
|
42 |
def writer_user(db, admin_user, sample_project): |
|
c588255…
|
ragelink
|
43 |
"""User with write access but not admin.""" |
|
c588255…
|
ragelink
|
44 |
writer = User.objects.create_user(username="writer", password="testpass123") |
|
c588255…
|
ragelink
|
45 |
team = Team.objects.create(name="Writers", organization=sample_project.organization, created_by=admin_user) |
|
c588255…
|
ragelink
|
46 |
team.members.add(writer) |
|
c588255…
|
ragelink
|
47 |
ProjectTeam.objects.create(project=sample_project, team=team, role="write", created_by=admin_user) |
|
c588255…
|
ragelink
|
48 |
return writer |
|
c588255…
|
ragelink
|
49 |
|
|
c588255…
|
ragelink
|
50 |
|
|
c588255…
|
ragelink
|
51 |
@pytest.fixture |
|
c588255…
|
ragelink
|
52 |
def writer_client(writer_user): |
|
c588255…
|
ragelink
|
53 |
client = Client() |
|
c588255…
|
ragelink
|
54 |
client.login(username="writer", password="testpass123") |
|
c588255…
|
ragelink
|
55 |
return client |
|
c588255…
|
ragelink
|
56 |
|
|
c588255…
|
ragelink
|
57 |
|
|
c588255…
|
ragelink
|
58 |
@pytest.fixture |
|
c588255…
|
ragelink
|
59 |
def reader_user(db, admin_user, sample_project): |
|
c588255…
|
ragelink
|
60 |
"""User with read access only.""" |
|
c588255…
|
ragelink
|
61 |
reader = User.objects.create_user(username="reader", password="testpass123") |
|
c588255…
|
ragelink
|
62 |
team = Team.objects.create(name="Readers", organization=sample_project.organization, created_by=admin_user) |
|
c588255…
|
ragelink
|
63 |
team.members.add(reader) |
|
c588255…
|
ragelink
|
64 |
ProjectTeam.objects.create(project=sample_project, team=team, role="read", created_by=admin_user) |
|
c588255…
|
ragelink
|
65 |
return reader |
|
c588255…
|
ragelink
|
66 |
|
|
c588255…
|
ragelink
|
67 |
|
|
c588255…
|
ragelink
|
68 |
@pytest.fixture |
|
c588255…
|
ragelink
|
69 |
def reader_client(reader_user): |
|
c588255…
|
ragelink
|
70 |
client = Client() |
|
c588255…
|
ragelink
|
71 |
client.login(username="reader", password="testpass123") |
|
c588255…
|
ragelink
|
72 |
return client |
|
c588255…
|
ragelink
|
73 |
|
|
c588255…
|
ragelink
|
74 |
|
|
c588255…
|
ragelink
|
75 |
# --- SQL Validation Tests --- |
|
c588255…
|
ragelink
|
76 |
|
|
c588255…
|
ragelink
|
77 |
|
|
c588255…
|
ragelink
|
78 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
79 |
class TestTicketReportSQLValidation: |
|
c588255…
|
ragelink
|
80 |
def test_valid_select(self): |
|
c588255…
|
ragelink
|
81 |
assert TicketReport.validate_sql("SELECT * FROM ticket") is None |
|
c588255…
|
ragelink
|
82 |
|
|
c588255…
|
ragelink
|
83 |
def test_valid_select_with_where(self): |
|
c588255…
|
ragelink
|
84 |
assert TicketReport.validate_sql("SELECT title, status FROM ticket WHERE status = 'Open'") is None |
|
c588255…
|
ragelink
|
85 |
|
|
c588255…
|
ragelink
|
86 |
def test_valid_select_with_join(self): |
|
c588255…
|
ragelink
|
87 |
assert TicketReport.validate_sql("SELECT t.title, tc.icomment FROM ticket t JOIN ticketchng tc ON t.tkt_id = tc.tkt_id") is None |
|
c588255…
|
ragelink
|
88 |
|
|
c588255…
|
ragelink
|
89 |
def test_reject_empty(self): |
|
c588255…
|
ragelink
|
90 |
assert TicketReport.validate_sql("") is not None |
|
c588255…
|
ragelink
|
91 |
assert "empty" in TicketReport.validate_sql("").lower() |
|
c588255…
|
ragelink
|
92 |
|
|
c588255…
|
ragelink
|
93 |
def test_reject_insert(self): |
|
c588255…
|
ragelink
|
94 |
error = TicketReport.validate_sql("INSERT INTO ticket (title) VALUES ('hack')") |
|
c588255…
|
ragelink
|
95 |
assert error is not None |
|
c588255…
|
ragelink
|
96 |
assert "select" in error.lower() or "forbidden" in error.lower() |
|
c588255…
|
ragelink
|
97 |
|
|
c588255…
|
ragelink
|
98 |
def test_reject_update(self): |
|
c588255…
|
ragelink
|
99 |
error = TicketReport.validate_sql("UPDATE ticket SET title = 'hacked'") |
|
c588255…
|
ragelink
|
100 |
assert error is not None |
|
c588255…
|
ragelink
|
101 |
|
|
c588255…
|
ragelink
|
102 |
def test_reject_delete(self): |
|
c588255…
|
ragelink
|
103 |
error = TicketReport.validate_sql("DELETE FROM ticket") |
|
c588255…
|
ragelink
|
104 |
assert error is not None |
|
c588255…
|
ragelink
|
105 |
|
|
c588255…
|
ragelink
|
106 |
def test_reject_drop(self): |
|
c588255…
|
ragelink
|
107 |
error = TicketReport.validate_sql("DROP TABLE ticket") |
|
c588255…
|
ragelink
|
108 |
assert error is not None |
|
c588255…
|
ragelink
|
109 |
|
|
c588255…
|
ragelink
|
110 |
def test_reject_alter(self): |
|
c588255…
|
ragelink
|
111 |
error = TicketReport.validate_sql("ALTER TABLE ticket ADD COLUMN evil TEXT") |
|
c588255…
|
ragelink
|
112 |
assert error is not None |
|
c588255…
|
ragelink
|
113 |
|
|
c588255…
|
ragelink
|
114 |
def test_reject_create(self): |
|
c588255…
|
ragelink
|
115 |
error = TicketReport.validate_sql("CREATE TABLE evil (id INTEGER)") |
|
c588255…
|
ragelink
|
116 |
assert error is not None |
|
c588255…
|
ragelink
|
117 |
|
|
c588255…
|
ragelink
|
118 |
def test_reject_attach(self): |
|
c588255…
|
ragelink
|
119 |
error = TicketReport.validate_sql("ATTACH DATABASE ':memory:' AS evil") |
|
c588255…
|
ragelink
|
120 |
assert error is not None |
|
c588255…
|
ragelink
|
121 |
|
|
c588255…
|
ragelink
|
122 |
def test_reject_pragma(self): |
|
c588255…
|
ragelink
|
123 |
error = TicketReport.validate_sql("PRAGMA table_info(ticket)") |
|
c588255…
|
ragelink
|
124 |
assert error is not None |
|
c588255…
|
ragelink
|
125 |
|
|
c588255…
|
ragelink
|
126 |
def test_reject_multiple_statements(self): |
|
c588255…
|
ragelink
|
127 |
error = TicketReport.validate_sql("SELECT 1; DROP TABLE ticket") |
|
c588255…
|
ragelink
|
128 |
assert error is not None |
|
c588255…
|
ragelink
|
129 |
# May be caught by forbidden keyword or multiple statement check |
|
c588255…
|
ragelink
|
130 |
assert "multiple" in error.lower() or "forbidden" in error.lower() |
|
c588255…
|
ragelink
|
131 |
|
|
c588255…
|
ragelink
|
132 |
def test_reject_multiple_statements_pure(self): |
|
c588255…
|
ragelink
|
133 |
"""Semicolons without forbidden keywords should also be rejected.""" |
|
c588255…
|
ragelink
|
134 |
error = TicketReport.validate_sql("SELECT 1; SELECT 2") |
|
c588255…
|
ragelink
|
135 |
assert error is not None |
|
c588255…
|
ragelink
|
136 |
assert "multiple" in error.lower() |
|
c588255…
|
ragelink
|
137 |
|
|
c588255…
|
ragelink
|
138 |
def test_reject_non_select_start(self): |
|
c588255…
|
ragelink
|
139 |
error = TicketReport.validate_sql("WITH cte AS (DELETE FROM ticket) SELECT * FROM cte") |
|
c588255…
|
ragelink
|
140 |
assert error is not None |
|
c588255…
|
ragelink
|
141 |
assert "SELECT" in error |
|
c588255…
|
ragelink
|
142 |
|
|
c588255…
|
ragelink
|
143 |
|
|
c588255…
|
ragelink
|
144 |
# --- Model Tests --- |
|
c588255…
|
ragelink
|
145 |
|
|
c588255…
|
ragelink
|
146 |
|
|
c588255…
|
ragelink
|
147 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
148 |
class TestTicketReportModel: |
|
c588255…
|
ragelink
|
149 |
def test_create_report(self, public_report): |
|
c588255…
|
ragelink
|
150 |
assert public_report.pk is not None |
|
c588255…
|
ragelink
|
151 |
assert str(public_report) == "Open Tickets" |
|
c588255…
|
ragelink
|
152 |
|
|
c588255…
|
ragelink
|
153 |
def test_soft_delete(self, public_report, admin_user): |
|
c588255…
|
ragelink
|
154 |
public_report.soft_delete(user=admin_user) |
|
c588255…
|
ragelink
|
155 |
assert public_report.is_deleted |
|
c588255…
|
ragelink
|
156 |
assert TicketReport.objects.filter(pk=public_report.pk).count() == 0 |
|
c588255…
|
ragelink
|
157 |
assert TicketReport.all_objects.filter(pk=public_report.pk).count() == 1 |
|
c588255…
|
ragelink
|
158 |
|
|
c588255…
|
ragelink
|
159 |
def test_ordering(self, fossil_repo_obj, admin_user): |
|
c588255…
|
ragelink
|
160 |
r_b = TicketReport.objects.create(repository=fossil_repo_obj, title="B Report", sql_query="SELECT 1", created_by=admin_user) |
|
c588255…
|
ragelink
|
161 |
r_a = TicketReport.objects.create(repository=fossil_repo_obj, title="A Report", sql_query="SELECT 1", created_by=admin_user) |
|
c588255…
|
ragelink
|
162 |
reports = list(TicketReport.objects.filter(repository=fossil_repo_obj)) |
|
c588255…
|
ragelink
|
163 |
assert reports[0] == r_a # alphabetical |
|
c588255…
|
ragelink
|
164 |
assert reports[1] == r_b |
|
c588255…
|
ragelink
|
165 |
|
|
c588255…
|
ragelink
|
166 |
|
|
c588255…
|
ragelink
|
167 |
# --- List View Tests --- |
|
c588255…
|
ragelink
|
168 |
|
|
c588255…
|
ragelink
|
169 |
|
|
c588255…
|
ragelink
|
170 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
171 |
class TestTicketReportsListView: |
|
c588255…
|
ragelink
|
172 |
def test_list_reports_admin(self, admin_client, sample_project, public_report, private_report): |
|
c588255…
|
ragelink
|
173 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/") |
|
c588255…
|
ragelink
|
174 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
175 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
176 |
assert "Open Tickets" in content |
|
c588255…
|
ragelink
|
177 |
assert "Internal Metrics" in content # admin sees private reports |
|
c588255…
|
ragelink
|
178 |
|
|
c588255…
|
ragelink
|
179 |
def test_list_reports_reader_hides_private(self, reader_client, sample_project, public_report, private_report): |
|
c588255…
|
ragelink
|
180 |
response = reader_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/") |
|
c588255…
|
ragelink
|
181 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
182 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
183 |
assert "Open Tickets" in content |
|
c588255…
|
ragelink
|
184 |
assert "Internal Metrics" not in content # reader cannot see private reports |
|
c588255…
|
ragelink
|
185 |
|
|
c588255…
|
ragelink
|
186 |
def test_list_empty(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
187 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/") |
|
c588255…
|
ragelink
|
188 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
189 |
assert "No ticket reports defined" in response.content.decode() |
|
c588255…
|
ragelink
|
190 |
|
|
c588255…
|
ragelink
|
191 |
def test_list_denied_for_anon(self, client, sample_project): |
|
c588255…
|
ragelink
|
192 |
response = client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/") |
|
c588255…
|
ragelink
|
193 |
# Private project: anonymous user gets 403 from require_project_read |
|
c588255…
|
ragelink
|
194 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
195 |
|
|
c588255…
|
ragelink
|
196 |
|
|
c588255…
|
ragelink
|
197 |
# --- Create View Tests --- |
|
c588255…
|
ragelink
|
198 |
|
|
c588255…
|
ragelink
|
199 |
|
|
c588255…
|
ragelink
|
200 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
201 |
class TestTicketReportCreateView: |
|
c588255…
|
ragelink
|
202 |
def test_get_form(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
203 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/create/") |
|
c588255…
|
ragelink
|
204 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
205 |
assert "Create Ticket Report" in response.content.decode() |
|
c588255…
|
ragelink
|
206 |
|
|
c588255…
|
ragelink
|
207 |
def test_create_report(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
208 |
response = admin_client.post( |
|
c588255…
|
ragelink
|
209 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/create/", |
|
c588255…
|
ragelink
|
210 |
{ |
|
c588255…
|
ragelink
|
211 |
"title": "Critical Bugs", |
|
c588255…
|
ragelink
|
212 |
"description": "All critical severity tickets", |
|
c588255…
|
ragelink
|
213 |
"sql_query": "SELECT tkt_uuid, title FROM ticket WHERE severity = 'Critical'", |
|
c588255…
|
ragelink
|
214 |
"is_public": "on", |
|
c588255…
|
ragelink
|
215 |
}, |
|
c588255…
|
ragelink
|
216 |
) |
|
c588255…
|
ragelink
|
217 |
assert response.status_code == 302 |
|
c588255…
|
ragelink
|
218 |
report = TicketReport.objects.get(title="Critical Bugs") |
|
c588255…
|
ragelink
|
219 |
assert report.is_public is True |
|
c588255…
|
ragelink
|
220 |
assert "Critical" in report.sql_query |
|
c588255…
|
ragelink
|
221 |
|
|
c588255…
|
ragelink
|
222 |
def test_create_report_rejects_dangerous_sql(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
223 |
response = admin_client.post( |
|
c588255…
|
ragelink
|
224 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/create/", |
|
c588255…
|
ragelink
|
225 |
{ |
|
c588255…
|
ragelink
|
226 |
"title": "Evil Report", |
|
c588255…
|
ragelink
|
227 |
"sql_query": "DROP TABLE ticket", |
|
c588255…
|
ragelink
|
228 |
}, |
|
c588255…
|
ragelink
|
229 |
) |
|
c588255…
|
ragelink
|
230 |
assert response.status_code == 200 # re-renders form with error |
|
c588255…
|
ragelink
|
231 |
assert TicketReport.objects.filter(title="Evil Report").count() == 0 |
|
c588255…
|
ragelink
|
232 |
|
|
c588255…
|
ragelink
|
233 |
def test_create_denied_for_writer(self, writer_client, sample_project): |
|
c588255…
|
ragelink
|
234 |
response = writer_client.post( |
|
c588255…
|
ragelink
|
235 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/create/", |
|
c588255…
|
ragelink
|
236 |
{"title": "Hack", "sql_query": "SELECT 1"}, |
|
c588255…
|
ragelink
|
237 |
) |
|
c588255…
|
ragelink
|
238 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
239 |
|
|
c588255…
|
ragelink
|
240 |
def test_create_denied_for_reader(self, reader_client, sample_project): |
|
c588255…
|
ragelink
|
241 |
response = reader_client.post( |
|
c588255…
|
ragelink
|
242 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/create/", |
|
c588255…
|
ragelink
|
243 |
{"title": "Hack", "sql_query": "SELECT 1"}, |
|
c588255…
|
ragelink
|
244 |
) |
|
c588255…
|
ragelink
|
245 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
246 |
|
|
c588255…
|
ragelink
|
247 |
|
|
c588255…
|
ragelink
|
248 |
# --- Edit View Tests --- |
|
c588255…
|
ragelink
|
249 |
|
|
c588255…
|
ragelink
|
250 |
|
|
c588255…
|
ragelink
|
251 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
252 |
class TestTicketReportEditView: |
|
c588255…
|
ragelink
|
253 |
def test_get_edit_form(self, admin_client, sample_project, public_report): |
|
c588255…
|
ragelink
|
254 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/{public_report.pk}/edit/") |
|
c588255…
|
ragelink
|
255 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
256 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
257 |
assert "Open Tickets" in content |
|
c588255…
|
ragelink
|
258 |
|
|
c588255…
|
ragelink
|
259 |
def test_edit_report(self, admin_client, sample_project, public_report): |
|
c588255…
|
ragelink
|
260 |
response = admin_client.post( |
|
c588255…
|
ragelink
|
261 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/{public_report.pk}/edit/", |
|
c588255…
|
ragelink
|
262 |
{ |
|
c588255…
|
ragelink
|
263 |
"title": "Open Tickets (Updated)", |
|
c588255…
|
ragelink
|
264 |
"description": "Updated description", |
|
c588255…
|
ragelink
|
265 |
"sql_query": "SELECT tkt_uuid, title, status FROM ticket WHERE status = 'Open' ORDER BY tkt_ctime DESC", |
|
c588255…
|
ragelink
|
266 |
"is_public": "on", |
|
c588255…
|
ragelink
|
267 |
}, |
|
c588255…
|
ragelink
|
268 |
) |
|
c588255…
|
ragelink
|
269 |
assert response.status_code == 302 |
|
c588255…
|
ragelink
|
270 |
public_report.refresh_from_db() |
|
c588255…
|
ragelink
|
271 |
assert public_report.title == "Open Tickets (Updated)" |
|
c588255…
|
ragelink
|
272 |
|
|
c588255…
|
ragelink
|
273 |
def test_edit_rejects_dangerous_sql(self, admin_client, sample_project, public_report): |
|
c588255…
|
ragelink
|
274 |
response = admin_client.post( |
|
c588255…
|
ragelink
|
275 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/{public_report.pk}/edit/", |
|
c588255…
|
ragelink
|
276 |
{ |
|
c588255…
|
ragelink
|
277 |
"title": "Open Tickets", |
|
c588255…
|
ragelink
|
278 |
"sql_query": "DELETE FROM ticket", |
|
c588255…
|
ragelink
|
279 |
}, |
|
c588255…
|
ragelink
|
280 |
) |
|
c588255…
|
ragelink
|
281 |
assert response.status_code == 200 # re-renders form |
|
c588255…
|
ragelink
|
282 |
public_report.refresh_from_db() |
|
c588255…
|
ragelink
|
283 |
assert "DELETE" not in public_report.sql_query |
|
c588255…
|
ragelink
|
284 |
|
|
c588255…
|
ragelink
|
285 |
def test_edit_denied_for_writer(self, writer_client, sample_project, public_report): |
|
c588255…
|
ragelink
|
286 |
response = writer_client.post( |
|
c588255…
|
ragelink
|
287 |
f"/projects/{sample_project.slug}/fossil/tickets/reports/{public_report.pk}/edit/", |
|
c588255…
|
ragelink
|
288 |
{"title": "Hacked", "sql_query": "SELECT 1"}, |
|
c588255…
|
ragelink
|
289 |
) |
|
c588255…
|
ragelink
|
290 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
291 |
|
|
c588255…
|
ragelink
|
292 |
def test_edit_nonexistent(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
293 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/99999/edit/") |
|
c588255…
|
ragelink
|
294 |
assert response.status_code == 404 |
|
c588255…
|
ragelink
|
295 |
|
|
c588255…
|
ragelink
|
296 |
|
|
c588255…
|
ragelink
|
297 |
# --- Run View Tests --- |
|
c588255…
|
ragelink
|
298 |
|
|
c588255…
|
ragelink
|
299 |
|
|
c588255…
|
ragelink
|
300 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
301 |
class TestTicketReportRunView: |
|
c588255…
|
ragelink
|
302 |
def test_run_public_report_as_reader(self, reader_client, sample_project, public_report): |
|
c588255…
|
ragelink
|
303 |
"""Readers can run public reports, but report may error if .fossil file not on disk.""" |
|
c588255…
|
ragelink
|
304 |
response = reader_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/{public_report.pk}/") |
|
c588255…
|
ragelink
|
305 |
# The .fossil file does not exist on disk in test env, so we get a database error |
|
c588255…
|
ragelink
|
306 |
# but the view itself should not raise a 403/404 — it renders the error in-page |
|
c588255…
|
ragelink
|
307 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
308 |
|
|
c588255…
|
ragelink
|
309 |
def test_run_private_report_denied_for_reader(self, reader_client, sample_project, private_report): |
|
c588255…
|
ragelink
|
310 |
response = reader_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/{private_report.pk}/") |
|
c588255…
|
ragelink
|
311 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
312 |
|
|
c588255…
|
ragelink
|
313 |
def test_run_private_report_allowed_for_admin(self, admin_client, sample_project, private_report): |
|
c588255…
|
ragelink
|
314 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/{private_report.pk}/") |
|
c588255…
|
ragelink
|
315 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
316 |
|
|
c588255…
|
ragelink
|
317 |
def test_run_nonexistent_report(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
318 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/99999/") |
|
c588255…
|
ragelink
|
319 |
assert response.status_code == 404 |
|
c588255…
|
ragelink
|
320 |
|
|
c588255…
|
ragelink
|
321 |
def test_run_denied_for_anon(self, client, sample_project, public_report): |
|
c588255…
|
ragelink
|
322 |
response = client.get(f"/projects/{sample_project.slug}/fossil/tickets/reports/{public_report.pk}/") |
|
c588255…
|
ragelink
|
323 |
# Private project: anonymous user gets 403 from require_project_read |
|
c588255…
|
ragelink
|
324 |
assert response.status_code == 403 |