|
c588255…
|
ragelink
|
1 |
import sqlite3 |
|
c588255…
|
ragelink
|
2 |
from pathlib import Path |
|
c588255…
|
ragelink
|
3 |
from unittest.mock import MagicMock, patch |
|
c588255…
|
ragelink
|
4 |
|
|
c588255…
|
ragelink
|
5 |
import pytest |
|
c588255…
|
ragelink
|
6 |
|
|
c588255…
|
ragelink
|
7 |
from fossil.models import FossilRepository |
|
c588255…
|
ragelink
|
8 |
from fossil.reader import FossilReader |
|
c588255…
|
ragelink
|
9 |
|
|
c588255…
|
ragelink
|
10 |
# Reusable patch that makes FossilRepository.exists_on_disk return True |
|
c588255…
|
ragelink
|
11 |
_disk_patch = patch("fossil.models.FossilRepository.exists_on_disk", new_callable=lambda: property(lambda self: True)) |
|
c588255…
|
ragelink
|
12 |
|
|
c588255…
|
ragelink
|
13 |
|
|
c588255…
|
ragelink
|
14 |
@pytest.fixture |
|
c588255…
|
ragelink
|
15 |
def fossil_repo_obj(sample_project): |
|
c588255…
|
ragelink
|
16 |
"""Return the auto-created FossilRepository for sample_project.""" |
|
c588255…
|
ragelink
|
17 |
return FossilRepository.objects.get(project=sample_project, deleted_at__isnull=True) |
|
c588255…
|
ragelink
|
18 |
|
|
c588255…
|
ragelink
|
19 |
|
|
c588255…
|
ragelink
|
20 |
def _create_test_fossil_db(path: Path): |
|
c588255…
|
ragelink
|
21 |
"""Create a minimal .fossil SQLite database with the tables reader.py needs.""" |
|
c588255…
|
ragelink
|
22 |
conn = sqlite3.connect(str(path)) |
|
c588255…
|
ragelink
|
23 |
conn.execute( |
|
c588255…
|
ragelink
|
24 |
""" |
|
c588255…
|
ragelink
|
25 |
CREATE TABLE blob ( |
|
c588255…
|
ragelink
|
26 |
rid INTEGER PRIMARY KEY, |
|
c588255…
|
ragelink
|
27 |
uuid TEXT UNIQUE NOT NULL, |
|
c588255…
|
ragelink
|
28 |
size INTEGER NOT NULL DEFAULT 0, |
|
c588255…
|
ragelink
|
29 |
content BLOB |
|
c588255…
|
ragelink
|
30 |
) |
|
c588255…
|
ragelink
|
31 |
""" |
|
c588255…
|
ragelink
|
32 |
) |
|
c588255…
|
ragelink
|
33 |
conn.execute( |
|
c588255…
|
ragelink
|
34 |
""" |
|
c588255…
|
ragelink
|
35 |
CREATE TABLE event ( |
|
c588255…
|
ragelink
|
36 |
type TEXT, |
|
c588255…
|
ragelink
|
37 |
mtime REAL, |
|
c588255…
|
ragelink
|
38 |
objid INTEGER, |
|
c588255…
|
ragelink
|
39 |
user TEXT, |
|
c588255…
|
ragelink
|
40 |
comment TEXT |
|
c588255…
|
ragelink
|
41 |
) |
|
c588255…
|
ragelink
|
42 |
""" |
|
c588255…
|
ragelink
|
43 |
) |
|
c588255…
|
ragelink
|
44 |
conn.commit() |
|
c588255…
|
ragelink
|
45 |
return conn |
|
c588255…
|
ragelink
|
46 |
|
|
c588255…
|
ragelink
|
47 |
|
|
c588255…
|
ragelink
|
48 |
def _insert_technote(conn, rid, uuid, mtime, user, comment, body_content=""): |
|
c588255…
|
ragelink
|
49 |
"""Insert a technote event and blob into the test database. |
|
c588255…
|
ragelink
|
50 |
|
|
c588255…
|
ragelink
|
51 |
Technotes use event.type = 'e'. The blob contains a Fossil wiki artifact |
|
c588255…
|
ragelink
|
52 |
format: header cards followed by W <size>\\n<content>\\nZ <hash>. |
|
c588255…
|
ragelink
|
53 |
""" |
|
c588255…
|
ragelink
|
54 |
import struct |
|
c588255…
|
ragelink
|
55 |
import zlib |
|
c588255…
|
ragelink
|
56 |
|
|
c588255…
|
ragelink
|
57 |
# Build a minimal Fossil wiki artifact containing the body |
|
c588255…
|
ragelink
|
58 |
artifact = f"D 2024-01-01T00:00:00\nU {user}\nW {len(body_content.encode('utf-8'))}\n{body_content}\nZ 0000000000000000" |
|
c588255…
|
ragelink
|
59 |
raw_bytes = artifact.encode("utf-8") |
|
c588255…
|
ragelink
|
60 |
|
|
c588255…
|
ragelink
|
61 |
# Fossil stores blobs with a 4-byte big-endian size prefix + zlib compressed content |
|
c588255…
|
ragelink
|
62 |
compressed = struct.pack(">I", len(raw_bytes)) + zlib.compress(raw_bytes) |
|
c588255…
|
ragelink
|
63 |
|
|
c588255…
|
ragelink
|
64 |
conn.execute("INSERT INTO blob (rid, uuid, size, content) VALUES (?, ?, ?, ?)", (rid, uuid, len(raw_bytes), compressed)) |
|
c588255…
|
ragelink
|
65 |
conn.execute("INSERT INTO event (type, mtime, objid, user, comment) VALUES ('e', ?, ?, ?, ?)", (mtime, rid, user, comment)) |
|
c588255…
|
ragelink
|
66 |
conn.commit() |
|
c588255…
|
ragelink
|
67 |
|
|
c588255…
|
ragelink
|
68 |
|
|
c588255…
|
ragelink
|
69 |
@pytest.fixture |
|
c588255…
|
ragelink
|
70 |
def fossil_db(tmp_path): |
|
c588255…
|
ragelink
|
71 |
"""Create a temporary .fossil file with technote data for reader tests.""" |
|
c588255…
|
ragelink
|
72 |
db_path = tmp_path / "test.fossil" |
|
c588255…
|
ragelink
|
73 |
conn = _create_test_fossil_db(db_path) |
|
c588255…
|
ragelink
|
74 |
_insert_technote(conn, 100, "abc123def456", 2460676.5, "admin", "First technote", "# Hello\n\nThis is the body.") |
|
c588255…
|
ragelink
|
75 |
_insert_technote(conn, 101, "xyz789ghi012", 2460677.5, "dev", "Second technote", "Another note body.") |
|
c588255…
|
ragelink
|
76 |
conn.close() |
|
c588255…
|
ragelink
|
77 |
return db_path |
|
c588255…
|
ragelink
|
78 |
|
|
c588255…
|
ragelink
|
79 |
|
|
c588255…
|
ragelink
|
80 |
def _make_reader_mock(**methods): |
|
c588255…
|
ragelink
|
81 |
"""Create a MagicMock that replaces FossilReader as a class. |
|
c588255…
|
ragelink
|
82 |
|
|
c588255…
|
ragelink
|
83 |
The returned mock supports: |
|
c588255…
|
ragelink
|
84 |
reader = FossilReader(path) # returns a mock instance |
|
c588255…
|
ragelink
|
85 |
with reader: # context manager |
|
c588255…
|
ragelink
|
86 |
reader.some_method() # returns configured value |
|
c588255…
|
ragelink
|
87 |
""" |
|
c588255…
|
ragelink
|
88 |
mock_cls = MagicMock() |
|
c588255…
|
ragelink
|
89 |
# The instance returned by calling the class |
|
c588255…
|
ragelink
|
90 |
instance = MagicMock() |
|
c588255…
|
ragelink
|
91 |
mock_cls.return_value = instance |
|
c588255…
|
ragelink
|
92 |
# Context manager support: __enter__ returns the same instance |
|
c588255…
|
ragelink
|
93 |
instance.__enter__ = MagicMock(return_value=instance) |
|
c588255…
|
ragelink
|
94 |
instance.__exit__ = MagicMock(return_value=False) |
|
c588255…
|
ragelink
|
95 |
for name, val in methods.items(): |
|
c588255…
|
ragelink
|
96 |
getattr(instance, name).return_value = val |
|
c588255…
|
ragelink
|
97 |
return mock_cls |
|
c588255…
|
ragelink
|
98 |
|
|
c588255…
|
ragelink
|
99 |
|
|
c588255…
|
ragelink
|
100 |
# --- Reader unit tests (no Django DB needed) --- |
|
c588255…
|
ragelink
|
101 |
|
|
c588255…
|
ragelink
|
102 |
|
|
c588255…
|
ragelink
|
103 |
class TestGetTechnotes: |
|
c588255…
|
ragelink
|
104 |
def test_returns_technotes(self, fossil_db): |
|
c588255…
|
ragelink
|
105 |
reader = FossilReader(fossil_db) |
|
c588255…
|
ragelink
|
106 |
with reader: |
|
c588255…
|
ragelink
|
107 |
notes = reader.get_technotes() |
|
c588255…
|
ragelink
|
108 |
assert len(notes) == 2 |
|
c588255…
|
ragelink
|
109 |
assert notes[0]["uuid"] == "xyz789ghi012" # Most recent first |
|
c588255…
|
ragelink
|
110 |
assert notes[1]["uuid"] == "abc123def456" |
|
c588255…
|
ragelink
|
111 |
|
|
c588255…
|
ragelink
|
112 |
def test_technote_fields(self, fossil_db): |
|
c588255…
|
ragelink
|
113 |
reader = FossilReader(fossil_db) |
|
c588255…
|
ragelink
|
114 |
with reader: |
|
c588255…
|
ragelink
|
115 |
notes = reader.get_technotes() |
|
c588255…
|
ragelink
|
116 |
note = notes[1] # The first inserted one |
|
c588255…
|
ragelink
|
117 |
assert note["user"] == "admin" |
|
c588255…
|
ragelink
|
118 |
assert note["comment"] == "First technote" |
|
c588255…
|
ragelink
|
119 |
assert note["timestamp"] is not None |
|
c588255…
|
ragelink
|
120 |
|
|
c588255…
|
ragelink
|
121 |
def test_empty_repo(self, tmp_path): |
|
c588255…
|
ragelink
|
122 |
db_path = tmp_path / "empty.fossil" |
|
c588255…
|
ragelink
|
123 |
conn = _create_test_fossil_db(db_path) |
|
c588255…
|
ragelink
|
124 |
conn.close() |
|
c588255…
|
ragelink
|
125 |
reader = FossilReader(db_path) |
|
c588255…
|
ragelink
|
126 |
with reader: |
|
c588255…
|
ragelink
|
127 |
notes = reader.get_technotes() |
|
c588255…
|
ragelink
|
128 |
assert notes == [] |
|
c588255…
|
ragelink
|
129 |
|
|
c588255…
|
ragelink
|
130 |
|
|
c588255…
|
ragelink
|
131 |
class TestGetTechnoteDetail: |
|
c588255…
|
ragelink
|
132 |
def test_returns_detail_with_body(self, fossil_db): |
|
c588255…
|
ragelink
|
133 |
reader = FossilReader(fossil_db) |
|
c588255…
|
ragelink
|
134 |
with reader: |
|
c588255…
|
ragelink
|
135 |
note = reader.get_technote_detail("abc123def456") |
|
c588255…
|
ragelink
|
136 |
assert note is not None |
|
c588255…
|
ragelink
|
137 |
assert note["uuid"] == "abc123def456" |
|
c588255…
|
ragelink
|
138 |
assert note["comment"] == "First technote" |
|
c588255…
|
ragelink
|
139 |
assert "# Hello" in note["body"] |
|
c588255…
|
ragelink
|
140 |
assert "This is the body." in note["body"] |
|
c588255…
|
ragelink
|
141 |
|
|
c588255…
|
ragelink
|
142 |
def test_prefix_match(self, fossil_db): |
|
c588255…
|
ragelink
|
143 |
reader = FossilReader(fossil_db) |
|
c588255…
|
ragelink
|
144 |
with reader: |
|
c588255…
|
ragelink
|
145 |
note = reader.get_technote_detail("abc123") |
|
c588255…
|
ragelink
|
146 |
assert note is not None |
|
c588255…
|
ragelink
|
147 |
assert note["uuid"] == "abc123def456" |
|
c588255…
|
ragelink
|
148 |
|
|
c588255…
|
ragelink
|
149 |
def test_not_found(self, fossil_db): |
|
c588255…
|
ragelink
|
150 |
reader = FossilReader(fossil_db) |
|
c588255…
|
ragelink
|
151 |
with reader: |
|
c588255…
|
ragelink
|
152 |
note = reader.get_technote_detail("nonexistent") |
|
c588255…
|
ragelink
|
153 |
assert note is None |
|
c588255…
|
ragelink
|
154 |
|
|
c588255…
|
ragelink
|
155 |
|
|
c588255…
|
ragelink
|
156 |
class TestGetUnversionedFiles: |
|
c588255…
|
ragelink
|
157 |
def test_returns_files(self, tmp_path): |
|
c588255…
|
ragelink
|
158 |
db_path = tmp_path / "uv.fossil" |
|
c588255…
|
ragelink
|
159 |
conn = _create_test_fossil_db(db_path) |
|
c588255…
|
ragelink
|
160 |
conn.execute( |
|
c588255…
|
ragelink
|
161 |
""" |
|
c588255…
|
ragelink
|
162 |
CREATE TABLE unversioned ( |
|
c588255…
|
ragelink
|
163 |
uvid INTEGER PRIMARY KEY AUTOINCREMENT, |
|
c588255…
|
ragelink
|
164 |
name TEXT UNIQUE, |
|
c588255…
|
ragelink
|
165 |
rcvid INTEGER, |
|
c588255…
|
ragelink
|
166 |
mtime DATETIME, |
|
c588255…
|
ragelink
|
167 |
hash TEXT, |
|
c588255…
|
ragelink
|
168 |
sz INTEGER, |
|
c588255…
|
ragelink
|
169 |
encoding INT, |
|
c588255…
|
ragelink
|
170 |
content BLOB |
|
c588255…
|
ragelink
|
171 |
) |
|
c588255…
|
ragelink
|
172 |
""" |
|
c588255…
|
ragelink
|
173 |
) |
|
c588255…
|
ragelink
|
174 |
conn.execute( |
|
c588255…
|
ragelink
|
175 |
"INSERT INTO unversioned (name, mtime, hash, sz, encoding, content) VALUES (?, ?, ?, ?, ?, ?)", |
|
c588255…
|
ragelink
|
176 |
("readme.txt", 1700000000, "abc123hash", 42, 0, b"file content"), |
|
c588255…
|
ragelink
|
177 |
) |
|
c588255…
|
ragelink
|
178 |
conn.execute( |
|
c588255…
|
ragelink
|
179 |
"INSERT INTO unversioned (name, mtime, hash, sz, encoding, content) VALUES (?, ?, ?, ?, ?, ?)", |
|
c588255…
|
ragelink
|
180 |
("bin/app.tar.gz", 1700001000, "def456hash", 1024, 0, b"tarball"), |
|
c588255…
|
ragelink
|
181 |
) |
|
c588255…
|
ragelink
|
182 |
conn.commit() |
|
c588255…
|
ragelink
|
183 |
conn.close() |
|
c588255…
|
ragelink
|
184 |
|
|
c588255…
|
ragelink
|
185 |
reader = FossilReader(db_path) |
|
c588255…
|
ragelink
|
186 |
with reader: |
|
c588255…
|
ragelink
|
187 |
files = reader.get_unversioned_files() |
|
c588255…
|
ragelink
|
188 |
assert len(files) == 2 |
|
c588255…
|
ragelink
|
189 |
assert files[0]["name"] == "bin/app.tar.gz" # Alphabetical |
|
c588255…
|
ragelink
|
190 |
assert files[1]["name"] == "readme.txt" |
|
c588255…
|
ragelink
|
191 |
assert files[1]["size"] == 42 |
|
c588255…
|
ragelink
|
192 |
assert files[1]["hash"] == "abc123hash" |
|
c588255…
|
ragelink
|
193 |
assert files[1]["mtime"] is not None |
|
c588255…
|
ragelink
|
194 |
|
|
c588255…
|
ragelink
|
195 |
def test_no_unversioned_table(self, tmp_path): |
|
c588255…
|
ragelink
|
196 |
"""Repos without unversioned content don't have the table -- should return empty.""" |
|
c588255…
|
ragelink
|
197 |
db_path = tmp_path / "no_uv.fossil" |
|
c588255…
|
ragelink
|
198 |
conn = _create_test_fossil_db(db_path) |
|
c588255…
|
ragelink
|
199 |
conn.close() |
|
c588255…
|
ragelink
|
200 |
reader = FossilReader(db_path) |
|
c588255…
|
ragelink
|
201 |
with reader: |
|
c588255…
|
ragelink
|
202 |
files = reader.get_unversioned_files() |
|
c588255…
|
ragelink
|
203 |
assert files == [] |
|
c588255…
|
ragelink
|
204 |
|
|
c588255…
|
ragelink
|
205 |
def test_deleted_files_excluded(self, tmp_path): |
|
c588255…
|
ragelink
|
206 |
"""Deleted UV files have empty hash -- should be excluded.""" |
|
c588255…
|
ragelink
|
207 |
db_path = tmp_path / "del_uv.fossil" |
|
c588255…
|
ragelink
|
208 |
conn = _create_test_fossil_db(db_path) |
|
c588255…
|
ragelink
|
209 |
conn.execute( |
|
c588255…
|
ragelink
|
210 |
""" |
|
c588255…
|
ragelink
|
211 |
CREATE TABLE unversioned ( |
|
c588255…
|
ragelink
|
212 |
uvid INTEGER PRIMARY KEY AUTOINCREMENT, |
|
c588255…
|
ragelink
|
213 |
name TEXT UNIQUE, rcvid INTEGER, mtime DATETIME, |
|
c588255…
|
ragelink
|
214 |
hash TEXT, sz INTEGER, encoding INT, content BLOB |
|
c588255…
|
ragelink
|
215 |
) |
|
c588255…
|
ragelink
|
216 |
""" |
|
c588255…
|
ragelink
|
217 |
) |
|
c588255…
|
ragelink
|
218 |
conn.execute( |
|
c588255…
|
ragelink
|
219 |
"INSERT INTO unversioned (name, mtime, hash, sz) VALUES (?, ?, ?, ?)", |
|
c588255…
|
ragelink
|
220 |
("alive.txt", 1700000000, "somehash", 10), |
|
c588255…
|
ragelink
|
221 |
) |
|
c588255…
|
ragelink
|
222 |
conn.execute( |
|
c588255…
|
ragelink
|
223 |
"INSERT INTO unversioned (name, mtime, hash, sz) VALUES (?, ?, ?, ?)", |
|
c588255…
|
ragelink
|
224 |
("deleted.txt", 1700000000, "", 0), |
|
c588255…
|
ragelink
|
225 |
) |
|
c588255…
|
ragelink
|
226 |
conn.commit() |
|
c588255…
|
ragelink
|
227 |
conn.close() |
|
c588255…
|
ragelink
|
228 |
|
|
c588255…
|
ragelink
|
229 |
reader = FossilReader(db_path) |
|
c588255…
|
ragelink
|
230 |
with reader: |
|
c588255…
|
ragelink
|
231 |
files = reader.get_unversioned_files() |
|
c588255…
|
ragelink
|
232 |
assert len(files) == 1 |
|
c588255…
|
ragelink
|
233 |
assert files[0]["name"] == "alive.txt" |
|
c588255…
|
ragelink
|
234 |
|
|
c588255…
|
ragelink
|
235 |
|
|
c588255…
|
ragelink
|
236 |
# --- View tests (Django DB needed) --- |
|
c588255…
|
ragelink
|
237 |
|
|
c588255…
|
ragelink
|
238 |
|
|
c588255…
|
ragelink
|
239 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
240 |
class TestTechnoteListView: |
|
c588255…
|
ragelink
|
241 |
def test_list_page_loads(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
242 |
mock = _make_reader_mock(get_technotes=[{"uuid": "abc123", "timestamp": "2024-01-01", "user": "admin", "comment": "Test note"}]) |
|
c588255…
|
ragelink
|
243 |
with _disk_patch, patch("fossil.views.FossilReader", mock): |
|
c588255…
|
ragelink
|
244 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/technotes/") |
|
c588255…
|
ragelink
|
245 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
246 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
247 |
assert "Technotes" in content |
|
c588255…
|
ragelink
|
248 |
assert "Test note" in content |
|
c588255…
|
ragelink
|
249 |
|
|
c588255…
|
ragelink
|
250 |
def test_list_shows_create_button_for_writer(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
251 |
mock = _make_reader_mock(get_technotes=[]) |
|
c588255…
|
ragelink
|
252 |
with _disk_patch, patch("fossil.views.FossilReader", mock): |
|
c588255…
|
ragelink
|
253 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/technotes/") |
|
c588255…
|
ragelink
|
254 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
255 |
assert "New Technote" in response.content.decode() |
|
c588255…
|
ragelink
|
256 |
|
|
c588255…
|
ragelink
|
257 |
def test_list_hides_create_button_for_reader(self, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
258 |
from django.contrib.auth.models import User |
|
c588255…
|
ragelink
|
259 |
from django.test import Client |
|
c588255…
|
ragelink
|
260 |
|
|
c588255…
|
ragelink
|
261 |
User.objects.create_user(username="reader_only", password="testpass123") |
|
c588255…
|
ragelink
|
262 |
c = Client() |
|
c588255…
|
ragelink
|
263 |
c.login(username="reader_only", password="testpass123") |
|
c588255…
|
ragelink
|
264 |
sample_project.visibility = "public" |
|
c588255…
|
ragelink
|
265 |
sample_project.save() |
|
c588255…
|
ragelink
|
266 |
mock = _make_reader_mock(get_technotes=[]) |
|
c588255…
|
ragelink
|
267 |
with _disk_patch, patch("fossil.views.FossilReader", mock): |
|
c588255…
|
ragelink
|
268 |
response = c.get(f"/projects/{sample_project.slug}/fossil/technotes/") |
|
c588255…
|
ragelink
|
269 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
270 |
assert "New Technote" not in response.content.decode() |
|
c588255…
|
ragelink
|
271 |
|
|
c588255…
|
ragelink
|
272 |
def test_list_denied_for_no_perm_on_private(self, no_perm_client, sample_project): |
|
c588255…
|
ragelink
|
273 |
response = no_perm_client.get(f"/projects/{sample_project.slug}/fossil/technotes/") |
|
c588255…
|
ragelink
|
274 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
275 |
|
|
c588255…
|
ragelink
|
276 |
|
|
c588255…
|
ragelink
|
277 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
278 |
class TestTechnoteCreateView: |
|
c588255…
|
ragelink
|
279 |
def test_get_form(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
280 |
with _disk_patch: |
|
c588255…
|
ragelink
|
281 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/technotes/create/") |
|
c588255…
|
ragelink
|
282 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
283 |
assert "New Technote" in response.content.decode() |
|
c588255…
|
ragelink
|
284 |
|
|
c588255…
|
ragelink
|
285 |
def test_create_technote(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
286 |
mock_cli = MagicMock() |
|
c588255…
|
ragelink
|
287 |
mock_cli.return_value.technote_create.return_value = True |
|
c588255…
|
ragelink
|
288 |
with _disk_patch, patch("fossil.cli.FossilCLI", mock_cli): |
|
c588255…
|
ragelink
|
289 |
response = admin_client.post( |
|
c588255…
|
ragelink
|
290 |
f"/projects/{sample_project.slug}/fossil/technotes/create/", |
|
c588255…
|
ragelink
|
291 |
{"title": "My Note", "body": "Note body content", "timestamp": ""}, |
|
c588255…
|
ragelink
|
292 |
) |
|
c588255…
|
ragelink
|
293 |
assert response.status_code == 302 # Redirect to list |
|
c588255…
|
ragelink
|
294 |
|
|
c588255…
|
ragelink
|
295 |
def test_create_denied_for_anon(self, client, sample_project): |
|
c588255…
|
ragelink
|
296 |
response = client.get(f"/projects/{sample_project.slug}/fossil/technotes/create/") |
|
c588255…
|
ragelink
|
297 |
assert response.status_code == 302 # Redirect to login |
|
c588255…
|
ragelink
|
298 |
|
|
c588255…
|
ragelink
|
299 |
def test_create_denied_for_no_perm(self, no_perm_client, sample_project): |
|
c588255…
|
ragelink
|
300 |
response = no_perm_client.post( |
|
c588255…
|
ragelink
|
301 |
f"/projects/{sample_project.slug}/fossil/technotes/create/", |
|
c588255…
|
ragelink
|
302 |
{"title": "Nope", "body": "denied"}, |
|
c588255…
|
ragelink
|
303 |
) |
|
c588255…
|
ragelink
|
304 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
305 |
|
|
c588255…
|
ragelink
|
306 |
|
|
c588255…
|
ragelink
|
307 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
308 |
class TestTechnoteDetailView: |
|
c588255…
|
ragelink
|
309 |
def test_detail_page(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
310 |
mock = _make_reader_mock( |
|
c588255…
|
ragelink
|
311 |
get_technote_detail={ |
|
c588255…
|
ragelink
|
312 |
"uuid": "abc123def456", |
|
c588255…
|
ragelink
|
313 |
"timestamp": "2024-01-01", |
|
c588255…
|
ragelink
|
314 |
"user": "admin", |
|
c588255…
|
ragelink
|
315 |
"comment": "Test technote", |
|
c588255…
|
ragelink
|
316 |
"body": "# Hello\n\nBody text.", |
|
c588255…
|
ragelink
|
317 |
} |
|
c588255…
|
ragelink
|
318 |
) |
|
c588255…
|
ragelink
|
319 |
with _disk_patch, patch("fossil.views.FossilReader", mock): |
|
c588255…
|
ragelink
|
320 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/technotes/abc123def456/") |
|
c588255…
|
ragelink
|
321 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
322 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
323 |
assert "Test technote" in content |
|
c588255…
|
ragelink
|
324 |
assert "Body text" in content |
|
c588255…
|
ragelink
|
325 |
|
|
c588255…
|
ragelink
|
326 |
def test_detail_not_found(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
327 |
mock = _make_reader_mock(get_technote_detail=None) |
|
c588255…
|
ragelink
|
328 |
with _disk_patch, patch("fossil.views.FossilReader", mock): |
|
c588255…
|
ragelink
|
329 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/technotes/nonexistent/") |
|
c588255…
|
ragelink
|
330 |
assert response.status_code == 404 |
|
c588255…
|
ragelink
|
331 |
|
|
c588255…
|
ragelink
|
332 |
def test_detail_denied_for_no_perm_on_private(self, no_perm_client, sample_project): |
|
c588255…
|
ragelink
|
333 |
response = no_perm_client.get(f"/projects/{sample_project.slug}/fossil/technotes/abc123/") |
|
c588255…
|
ragelink
|
334 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
335 |
|
|
c588255…
|
ragelink
|
336 |
|
|
c588255…
|
ragelink
|
337 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
338 |
class TestTechnoteEditView: |
|
c588255…
|
ragelink
|
339 |
def test_get_edit_form(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
340 |
mock = _make_reader_mock( |
|
c588255…
|
ragelink
|
341 |
get_technote_detail={ |
|
c588255…
|
ragelink
|
342 |
"uuid": "abc123def456", |
|
c588255…
|
ragelink
|
343 |
"timestamp": "2024-01-01", |
|
c588255…
|
ragelink
|
344 |
"user": "admin", |
|
c588255…
|
ragelink
|
345 |
"comment": "Test technote", |
|
c588255…
|
ragelink
|
346 |
"body": "Existing body content", |
|
c588255…
|
ragelink
|
347 |
} |
|
c588255…
|
ragelink
|
348 |
) |
|
c588255…
|
ragelink
|
349 |
with _disk_patch, patch("fossil.views.FossilReader", mock): |
|
c588255…
|
ragelink
|
350 |
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/technotes/abc123def456/edit/") |
|
c588255…
|
ragelink
|
351 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
352 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
353 |
assert "Existing body content" in content |
|
c588255…
|
ragelink
|
354 |
|
|
c588255…
|
ragelink
|
355 |
def test_edit_technote(self, admin_client, sample_project, fossil_repo_obj): |
|
c588255…
|
ragelink
|
356 |
mock = _make_reader_mock( |
|
c588255…
|
ragelink
|
357 |
get_technote_detail={ |
|
c588255…
|
ragelink
|
358 |
"uuid": "abc123def456", |
|
c588255…
|
ragelink
|
359 |
"timestamp": "2024-01-01", |
|
c588255…
|
ragelink
|
360 |
"user": "admin", |
|
c588255…
|
ragelink
|
361 |
"comment": "Test technote", |
|
c588255…
|
ragelink
|
362 |
"body": "Old body", |
|
c588255…
|
ragelink
|
363 |
} |
|
c588255…
|
ragelink
|
364 |
) |
|
c588255…
|
ragelink
|
365 |
mock_cli = MagicMock() |
|
c588255…
|
ragelink
|
366 |
mock_cli.return_value.technote_edit.return_value = True |
|
c588255…
|
ragelink
|
367 |
with _disk_patch, patch("fossil.views.FossilReader", mock), patch("fossil.cli.FossilCLI", mock_cli): |
|
c588255…
|
ragelink
|
368 |
response = admin_client.post( |
|
c588255…
|
ragelink
|
369 |
f"/projects/{sample_project.slug}/fossil/technotes/abc123def456/edit/", |
|
c588255…
|
ragelink
|
370 |
{"body": "Updated body content"}, |
|
c588255…
|
ragelink
|
371 |
) |
|
c588255…
|
ragelink
|
372 |
assert response.status_code == 302 # Redirect to detail |
|
c588255…
|
ragelink
|
373 |
|
|
c588255…
|
ragelink
|
374 |
def test_edit_denied_for_anon(self, client, sample_project): |
|
c588255…
|
ragelink
|
375 |
response = client.get(f"/projects/{sample_project.slug}/fossil/technotes/abc123/edit/") |
|
c588255…
|
ragelink
|
376 |
assert response.status_code == 302 # Redirect to login |
|
c588255…
|
ragelink
|
377 |
|
|
c588255…
|
ragelink
|
378 |
def test_edit_denied_for_no_perm(self, no_perm_client, sample_project): |
|
c588255…
|
ragelink
|
379 |
response = no_perm_client.post( |
|
c588255…
|
ragelink
|
380 |
f"/projects/{sample_project.slug}/fossil/technotes/abc123/edit/", |
|
c588255…
|
ragelink
|
381 |
{"body": "denied"}, |
|
c588255…
|
ragelink
|
382 |
) |
|
c588255…
|
ragelink
|
383 |
assert response.status_code == 403 |