Skip to main content

max / makenotwork

10.4 KB · 389 lines History Blame Raw
1 //! Content lifecycle: create item -> add text content -> update -> delete -> soft-deleted
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 #[tokio::test]
7 async fn item_lifecycle() {
8 let mut h = TestHarness::new().await;
9
10 // Setup: creator with project
11 let user_id = h
12 .signup("author", "author@example.com", "password123")
13 .await;
14 h.grant_creator(user_id).await;
15 h.client.post_form("/logout", "").await;
16 h.login("author", "password123").await;
17
18 let resp = h
19 .client
20 .post_form("/api/projects", "slug=my-project&title=My+Project")
21 .await;
22 let project: Value = resp.json();
23 let project_id = project["id"].as_str().unwrap();
24
25 // Create item
26 let resp = h
27 .client
28 .post_form(
29 &format!("/api/projects/{project_id}/items"),
30 "title=My+Article&item_type=text",
31 )
32 .await;
33 assert!(
34 resp.status.is_success(),
35 "Create item failed: {}",
36 resp.text
37 );
38 let item: Value = resp.json();
39 let item_id = item["id"].as_str().unwrap();
40
41 // Add text content
42 let resp = h
43 .client
44 .put_json(
45 &format!("/api/items/{item_id}/text"),
46 "{\"body\": \"# Hello\\n\\nThis is my article content.\"}",
47 )
48 .await;
49 assert!(
50 resp.status.is_success(),
51 "Update text failed: {} {}",
52 resp.status,
53 resp.text
54 );
55 let text_resp: Value = resp.json();
56 assert!(text_resp["word_count"].as_u64().unwrap() > 0);
57
58 // Update text content
59 let resp = h
60 .client
61 .put_json(
62 &format!("/api/items/{item_id}/text"),
63 "{\"body\": \"# Updated\\n\\nRevised article content with more words.\"}",
64 )
65 .await;
66 assert!(
67 resp.status.is_success(),
68 "Update text failed: {}",
69 resp.text
70 );
71
72 let resp = h.client.delete(&format!("/api/items/{item_id}")).await;
73 assert!(
74 resp.status.is_success(),
75 "Delete item failed: {}",
76 resp.text
77 );
78
79 // Verify item is soft-deleted (deleted_at set, not visible to normal queries)
80 let deleted_at: Option<chrono::DateTime<chrono::Utc>> =
81 sqlx::query_scalar("SELECT deleted_at FROM items WHERE id = $1")
82 .bind(item_id.parse::<uuid::Uuid>().unwrap())
83 .fetch_one(&h.db)
84 .await
85 .unwrap();
86 assert!(
87 deleted_at.is_some(),
88 "Item should be soft-deleted (deleted_at set)"
89 );
90
91 // Verify item is not visible to normal listing queries
92 let visible_count = sqlx::query_scalar::<_, i64>(
93 "SELECT COUNT(*) FROM items WHERE id = $1 AND deleted_at IS NULL",
94 )
95 .bind(item_id.parse::<uuid::Uuid>().unwrap())
96 .fetch_one(&h.db)
97 .await
98 .unwrap();
99 assert_eq!(visible_count, 0, "Soft-deleted item should not be visible");
100 }
101
102 #[tokio::test]
103 async fn item_text_update() {
104 let mut h = TestHarness::new().await;
105
106 let user_id = h
107 .signup("textwriter", "textwriter@example.com", "password123")
108 .await;
109 h.grant_creator(user_id).await;
110 h.client.post_form("/logout", "").await;
111 h.login("textwriter", "password123").await;
112
113 let resp = h
114 .client
115 .post_form("/api/projects", "slug=text-proj&title=Text+Project")
116 .await;
117 let project: Value = resp.json();
118 let project_id = project["id"].as_str().unwrap();
119
120 // Create a text item
121 let resp = h
122 .client
123 .post_form(
124 &format!("/api/projects/{project_id}/items"),
125 "title=Text+Article&item_type=text",
126 )
127 .await;
128 assert!(
129 resp.status.is_success(),
130 "Create item failed: {}",
131 resp.text
132 );
133 let item: Value = resp.json();
134 let item_id = item["id"].as_str().unwrap();
135
136 // Set text body
137 let resp = h
138 .client
139 .put_json(
140 &format!("/api/items/{item_id}/text"),
141 r##"{"body": "# First Draft\n\nSome initial content here."}"##,
142 )
143 .await;
144 assert!(
145 resp.status.is_success(),
146 "Set text failed: {} {}",
147 resp.status,
148 resp.text
149 );
150 let text: Value = resp.json();
151 assert!(
152 text["word_count"].as_i64().unwrap() > 0,
153 "Word count should be positive"
154 );
155
156 // Update text body
157 let resp = h
158 .client
159 .put_json(
160 &format!("/api/items/{item_id}/text"),
161 r##"{"body": "# Revised Draft\n\nCompletely rewritten with new material and extra words."}"##,
162 )
163 .await;
164 assert!(
165 resp.status.is_success(),
166 "Update text failed: {} {}",
167 resp.status,
168 resp.text
169 );
170 let text: Value = resp.json();
171 assert_eq!(
172 text["body"].as_str(),
173 Some("# Revised Draft\n\nCompletely rewritten with new material and extra words."),
174 "Body should reflect the update"
175 );
176 }
177
178 #[tokio::test]
179 async fn item_duplicate() {
180 let mut h = TestHarness::new().await;
181
182 let user_id = h
183 .signup("dupuser", "dupuser@example.com", "password123")
184 .await;
185 h.grant_creator(user_id).await;
186 h.client.post_form("/logout", "").await;
187 h.login("dupuser", "password123").await;
188
189 let resp = h
190 .client
191 .post_form("/api/projects", "slug=dup-proj&title=Dup+Project")
192 .await;
193 let project: Value = resp.json();
194 let project_id = project["id"].as_str().unwrap();
195
196 // Create item with title, description, price
197 let resp = h
198 .client
199 .post_form(
200 &format!("/api/projects/{project_id}/items"),
201 "title=Original+Item&item_type=text&price_cents=500",
202 )
203 .await;
204 assert!(
205 resp.status.is_success(),
206 "Create item failed: {}",
207 resp.text
208 );
209 let item: Value = resp.json();
210 let item_id = item["id"].as_str().unwrap();
211
212 // Add a description
213 let resp = h
214 .client
215 .put_form(&format!("/api/items/{item_id}"), "description=A+great+item")
216 .await;
217 assert!(
218 resp.status.is_success(),
219 "Update item failed: {}",
220 resp.text
221 );
222
223 // Duplicate
224 let resp = h
225 .client
226 .post_json(&format!("/api/items/{item_id}/duplicate"), "{}")
227 .await;
228 assert!(
229 resp.status.is_success(),
230 "Duplicate failed: {} {}",
231 resp.status,
232 resp.text
233 );
234 let dup: Value = resp.json();
235
236 // Verify the duplicate has "Copy of" prefix and is a draft
237 let dup_title = dup["title"].as_str().unwrap();
238 assert!(
239 dup_title.starts_with("Copy of"),
240 "Duplicate title should start with 'Copy of', got: {dup_title}"
241 );
242 assert_eq!(
243 dup["is_public"].as_bool(),
244 Some(false),
245 "Duplicate should be a draft"
246 );
247 assert_eq!(
248 dup["price_cents"].as_i64(),
249 Some(500),
250 "Duplicate should preserve price"
251 );
252 assert_ne!(
253 dup["id"].as_str(),
254 Some(item_id),
255 "Duplicate should have a new ID"
256 );
257 }
258
259 #[tokio::test]
260 async fn non_owner_cannot_edit_item() {
261 let mut h = TestHarness::new().await;
262
263 // User A creates project + item
264 let user_a = h
265 .signup("itemowner", "itemowner@example.com", "password123")
266 .await;
267 h.grant_creator(user_a).await;
268 h.client.post_form("/logout", "").await;
269 h.login("itemowner", "password123").await;
270
271 let resp = h
272 .client
273 .post_form("/api/projects", "slug=owner-proj&title=Owner+Project")
274 .await;
275 let project: Value = resp.json();
276 let project_id = project["id"].as_str().unwrap();
277
278 let resp = h
279 .client
280 .post_form(
281 &format!("/api/projects/{project_id}/items"),
282 "title=Private+Item&item_type=text",
283 )
284 .await;
285 assert!(
286 resp.status.is_success(),
287 "Create item failed: {}",
288 resp.text
289 );
290 let item: Value = resp.json();
291 let item_id = item["id"].as_str().unwrap();
292
293 // Log out, sign up user B
294 h.client.post_form("/logout", "").await;
295 let user_b = h
296 .signup("itemintruder", "itemintruder@example.com", "password123")
297 .await;
298 h.grant_creator(user_b).await;
299 h.client.post_form("/logout", "").await;
300 h.login("itemintruder", "password123").await;
301
302 // User B tries to update user A's item
303 let resp = h
304 .client
305 .put_form(&format!("/api/items/{item_id}"), "title=Hacked+Item")
306 .await;
307 assert_eq!(
308 resp.status, 403,
309 "Non-owner update should be 403, got {}",
310 resp.status
311 );
312 }
313
314 #[tokio::test]
315 async fn publish_unpublish_item() {
316 let mut h = TestHarness::new().await;
317
318 let user_id = h
319 .signup("pubuser", "pubuser@example.com", "password123")
320 .await;
321 h.grant_creator(user_id).await;
322 h.client.post_form("/logout", "").await;
323 h.login("pubuser", "password123").await;
324
325 let resp = h
326 .client
327 .post_form("/api/projects", "slug=pub-proj&title=Pub+Project")
328 .await;
329 let project: Value = resp.json();
330 let project_id = project["id"].as_str().unwrap();
331
332 // Create item (public by default per DB schema)
333 let resp = h
334 .client
335 .post_form(
336 &format!("/api/projects/{project_id}/items"),
337 "title=Toggle+Item&item_type=text",
338 )
339 .await;
340 assert!(
341 resp.status.is_success(),
342 "Create item failed: {}",
343 resp.text
344 );
345 let item: Value = resp.json();
346 let item_id = item["id"].as_str().unwrap();
347 assert_eq!(
348 item["is_public"].as_bool(),
349 Some(true),
350 "New item should be public by default"
351 );
352
353 // Unpublish (make draft)
354 let resp = h
355 .client
356 .put_form(&format!("/api/items/{item_id}"), "is_public=false")
357 .await;
358 assert!(
359 resp.status.is_success(),
360 "Unpublish failed: {} {}",
361 resp.status,
362 resp.text
363 );
364 let updated: Value = resp.json();
365 assert_eq!(
366 updated["is_public"].as_bool(),
367 Some(false),
368 "Item should be draft after unpublish"
369 );
370
371 // Republish
372 let resp = h
373 .client
374 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
375 .await;
376 assert!(
377 resp.status.is_success(),
378 "Publish failed: {} {}",
379 resp.status,
380 resp.text
381 );
382 let updated: Value = resp.json();
383 assert_eq!(
384 updated["is_public"].as_bool(),
385 Some(true),
386 "Item should be public after republish"
387 );
388 }
389