Skip to main content

max / makenotwork

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