Skip to main content

max / makenotwork

10.2 KB · 343 lines History Blame Raw
1 //! Versions: create, is_current toggling, ordering, ownership, validation, draft visibility.
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 /// Helper: create a creator with a project and a digital item, return (project_id, item_id).
7 async fn setup_creator_with_digital_item(
8 h: &mut TestHarness,
9 username: &str,
10 _email: &str,
11 ) -> (String, String) {
12 let setup = h.create_creator_with_item(username, "digital", 0).await;
13 (setup.project_id, setup.item_id)
14 }
15
16 #[tokio::test]
17 async fn version_create_and_is_current() {
18 let mut h = TestHarness::new().await;
19 let (_project_id, item_id) =
20 setup_creator_with_digital_item(&mut h, "vcreator", "vcreator@test.com").await;
21
22 // Create v1
23 let resp = h
24 .client
25 .post_json(
26 &format!("/api/items/{item_id}/versions"),
27 r#"{"version_number": "1.0.0", "changelog": "Initial release"}"#,
28 )
29 .await;
30 assert_eq!(
31 resp.status, 200,
32 "Create v1 failed: {} {}",
33 resp.status, resp.text
34 );
35 let v1: Value = resp.json();
36 let v1_id = v1["id"].as_str().unwrap().to_string();
37 assert!(
38 v1["is_current"].as_bool().unwrap(),
39 "v1 should be current on creation"
40 );
41
42 // Create v2
43 let resp = h
44 .client
45 .post_json(
46 &format!("/api/items/{item_id}/versions"),
47 r#"{"version_number": "2.0.0", "changelog": "Major update"}"#,
48 )
49 .await;
50 assert_eq!(
51 resp.status, 200,
52 "Create v2 failed: {} {}",
53 resp.status, resp.text
54 );
55 let v2: Value = resp.json();
56 assert!(v2["is_current"].as_bool().unwrap(), "v2 should be current");
57
58 // Verify v1 is no longer current
59 let v1_current: bool = sqlx::query_scalar("SELECT is_current FROM versions WHERE id = $1")
60 .bind(v1_id.parse::<uuid::Uuid>().unwrap())
61 .fetch_one(&h.db)
62 .await
63 .unwrap();
64 assert!(
65 !v1_current,
66 "v1 should no longer be current after v2 was created"
67 );
68 }
69
70 #[tokio::test]
71 async fn version_list_newest_first() {
72 let mut h = TestHarness::new().await;
73 let (project_id, item_id) =
74 setup_creator_with_digital_item(&mut h, "vlist", "vlist@test.com").await;
75
76 // Create 3 versions
77 for ver in ["1.0.0", "1.1.0", "2.0.0"] {
78 let body = format!(r#"{{"version_number": "{ver}"}}"#);
79 let resp = h
80 .client
81 .post_json(&format!("/api/items/{item_id}/versions"), &body)
82 .await;
83 assert_eq!(
84 resp.status, 200,
85 "Create version {ver} failed: {} {}",
86 resp.status, resp.text
87 );
88 }
89
90 // Make item public so list endpoint works
91 h.client
92 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
93 .await;
94 h.client
95 .put_json(
96 &format!("/api/projects/{project_id}"),
97 r#"{"is_public": true}"#,
98 )
99 .await;
100
101 // List versions, newest first (ORDER BY created_at DESC)
102 let resp = h
103 .client
104 .get(&format!("/api/items/{item_id}/versions"))
105 .await;
106 assert_eq!(
107 resp.status, 200,
108 "List versions failed: {} {}",
109 resp.status, resp.text
110 );
111 let list: Value = resp.json();
112 let data = list["data"].as_array().unwrap();
113 assert_eq!(data.len(), 3);
114 assert_eq!(data[0]["version_number"].as_str().unwrap(), "2.0.0");
115 assert_eq!(data[1]["version_number"].as_str().unwrap(), "1.1.0");
116 assert_eq!(data[2]["version_number"].as_str().unwrap(), "1.0.0");
117 }
118
119 #[tokio::test]
120 async fn version_ownership_enforced() {
121 let mut h = TestHarness::new().await;
122 let (_project_id, item_id) =
123 setup_creator_with_digital_item(&mut h, "vowner", "vowner@test.com").await;
124
125 // Switch to creator B
126 h.client.post_form("/logout", "").await;
127 let b_id = h
128 .signup("vintruder", "vintruder@test.com", "password123")
129 .await;
130 h.grant_creator(b_id).await;
131 h.client.post_form("/logout", "").await;
132 h.login("vintruder", "password123").await;
133
134 // Creator B tries to create a version on A's item
135 let resp = h
136 .client
137 .post_json(
138 &format!("/api/items/{item_id}/versions"),
139 r#"{"version_number": "9.9.9"}"#,
140 )
141 .await;
142 assert_eq!(
143 resp.status, 403,
144 "Non-owner should get 403 on POST version, got {} {}",
145 resp.status, resp.text
146 );
147 }
148
149 #[tokio::test]
150 async fn version_validation() {
151 let mut h = TestHarness::new().await;
152 let (_project_id, item_id) =
153 setup_creator_with_digital_item(&mut h, "vvalid", "vvalid@test.com").await;
154
155 // Empty version_number
156 let resp = h
157 .client
158 .post_json(
159 &format!("/api/items/{item_id}/versions"),
160 r#"{"version_number": ""}"#,
161 )
162 .await;
163 assert!(
164 resp.status == 400 || resp.status == 422,
165 "Empty version_number should be rejected, got {} {}",
166 resp.status,
167 resp.text
168 );
169 }
170
171 #[tokio::test]
172 async fn list_versions_requires_public_item() {
173 let mut h = TestHarness::new().await;
174 let (_project_id, item_id) =
175 setup_creator_with_digital_item(&mut h, "vdraft", "vdraft@test.com").await;
176
177 // Create a version, then make item non-public
178 let resp = h
179 .client
180 .post_json(
181 &format!("/api/items/{item_id}/versions"),
182 r#"{"version_number": "0.1.0"}"#,
183 )
184 .await;
185 assert_eq!(resp.status, 200, "{}", resp.text);
186
187 // Mark item as draft (not public)
188 h.client
189 .put_form(&format!("/api/items/{item_id}"), "is_public=false")
190 .await;
191
192 // Logout, unauthenticated user tries to list versions of draft item
193 h.client.post_form("/logout", "").await;
194 h.client.fetch_csrf_token().await;
195
196 let resp = h
197 .client
198 .get(&format!("/api/items/{item_id}/versions"))
199 .await;
200 assert_eq!(
201 resp.status, 404,
202 "Draft item versions should return 404, got {} {}",
203 resp.status, resp.text
204 );
205 }
206
207 #[tokio::test]
208 async fn version_changelog_too_long() {
209 let mut h = TestHarness::new().await;
210 let (_project_id, item_id) =
211 setup_creator_with_digital_item(&mut h, "vchangelog", "vchangelog@test.com").await;
212
213 // changelog > 10,000 chars
214 let long_changelog = "x".repeat(10_001);
215 let body = serde_json::json!({
216 "version_number": "1.0.0",
217 "changelog": long_changelog,
218 });
219 let resp = h
220 .client
221 .post_json(&format!("/api/items/{item_id}/versions"), &body.to_string())
222 .await;
223 assert!(
224 resp.status == 400 || resp.status == 422,
225 "Changelog >10000 chars should be rejected, got {} {}",
226 resp.status,
227 resp.text
228 );
229 }
230
231 #[tokio::test]
232 async fn version_number_too_long() {
233 let mut h = TestHarness::new().await;
234 let (_project_id, item_id) =
235 setup_creator_with_digital_item(&mut h, "vlong", "vlong@test.com").await;
236
237 // version_number > 50 chars
238 let long_ver = "v".repeat(51);
239 let body = serde_json::json!({
240 "version_number": long_ver,
241 });
242 let resp = h
243 .client
244 .post_json(&format!("/api/items/{item_id}/versions"), &body.to_string())
245 .await;
246 assert!(
247 resp.status == 400 || resp.status == 422,
248 "Version number >50 chars should be rejected, got {} {}",
249 resp.status,
250 resp.text
251 );
252 }
253
254 #[tokio::test]
255 async fn version_number_boundary_succeeds() {
256 let mut h = TestHarness::new().await;
257 let (_project_id, item_id) =
258 setup_creator_with_digital_item(&mut h, "vbound", "vbound@test.com").await;
259
260 // Exactly 50 chars, should succeed
261 let ver_50 = "v".repeat(50);
262 let body = serde_json::json!({
263 "version_number": ver_50,
264 });
265 let resp = h
266 .client
267 .post_json(&format!("/api/items/{item_id}/versions"), &body.to_string())
268 .await;
269 assert_eq!(
270 resp.status, 200,
271 "50-char version number should be accepted, got {} {}",
272 resp.status, resp.text
273 );
274 let version: Value = resp.json();
275 assert_eq!(version["version_number"].as_str().unwrap(), ver_50);
276 }
277
278 #[tokio::test]
279 async fn version_optional_fields_preserved() {
280 let mut h = TestHarness::new().await;
281 let (_project_id, item_id) =
282 setup_creator_with_digital_item(&mut h, "vfields", "vfields@test.com").await;
283
284 let body = serde_json::json!({
285 "version_number": "3.0.0",
286 "changelog": "Added widgets",
287 "file_url": "https://example.com/app-3.0.0.zip",
288 "file_size_bytes": 1_048_576_i64,
289 "file_name": "app-3.0.0.zip",
290 });
291 let resp = h
292 .client
293 .post_json(&format!("/api/items/{item_id}/versions"), &body.to_string())
294 .await;
295 assert_eq!(
296 resp.status, 200,
297 "Create version with optional fields failed: {} {}",
298 resp.status, resp.text
299 );
300 let version: Value = resp.json();
301 let version_id = version["id"].as_str().unwrap();
302
303 // Verify fields in response
304 assert_eq!(version["changelog"].as_str().unwrap(), "Added widgets");
305 assert_eq!(
306 version["file_url"].as_str().unwrap(),
307 "https://example.com/app-3.0.0.zip"
308 );
309
310 // Verify file_size_bytes and file_name via direct SQL (not in VersionResponse)
311 let (file_size, file_name): (Option<i64>, Option<String>) =
312 sqlx::query_as("SELECT file_size_bytes, file_name FROM versions WHERE id = $1")
313 .bind(version_id.parse::<uuid::Uuid>().unwrap())
314 .fetch_one(&h.db)
315 .await
316 .unwrap();
317 assert_eq!(file_size, Some(1_048_576));
318 assert_eq!(file_name.as_deref(), Some("app-3.0.0.zip"));
319 }
320
321 #[tokio::test]
322 async fn version_unauthenticated_rejected() {
323 let mut h = TestHarness::new().await;
324 let (_project_id, item_id) =
325 setup_creator_with_digital_item(&mut h, "vunauth", "vunauth@test.com").await;
326
327 h.client.post_form("/logout", "").await;
328 h.client.fetch_csrf_token().await;
329
330 let resp = h
331 .client
332 .post_json(
333 &format!("/api/items/{item_id}/versions"),
334 r#"{"version_number": "1.0.0"}"#,
335 )
336 .await;
337 assert_eq!(
338 resp.status, 401,
339 "Unauthenticated POST version should be 401, got {} {}",
340 resp.status, resp.text
341 );
342 }
343