Skip to main content

max / makenotwork

6.6 KB · 215 lines History Blame Raw
1 //! Project management workflow tests — CRUD, update, delete cascade.
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 /// Helper: create a creator and return user_id.
7 async fn setup_creator(h: &mut TestHarness, username: &str) -> String {
8 h.create_creator(username).await.to_string()
9 }
10
11 #[tokio::test]
12 async fn create_project_returns_slug() {
13 let mut h = TestHarness::new().await;
14 setup_creator(&mut h, "projcreate").await;
15
16 let resp = h
17 .client
18 .post_form("/api/projects", "slug=my-cool-project&title=Cool+Project")
19 .await;
20 assert!(resp.status.is_success(), "Create project failed: {}", resp.text);
21 let project: Value = resp.json();
22 assert_eq!(project["slug"].as_str().unwrap(), "my-cool-project");
23 assert_eq!(project["title"].as_str().unwrap(), "Cool Project");
24 }
25
26 #[tokio::test]
27 async fn create_project_requires_creator() {
28 let mut h = TestHarness::new().await;
29 let _user_id = h.signup("projnoauth", "projnoauth@test.com", "password123").await;
30
31 let resp = h
32 .client
33 .post_form("/api/projects", "slug=blocked&title=Blocked")
34 .await;
35 assert!(
36 resp.status.is_client_error(),
37 "Non-creator should be rejected: {} {}",
38 resp.status, resp.text
39 );
40 }
41
42 #[tokio::test]
43 async fn update_project_title_and_description() {
44 let mut h = TestHarness::new().await;
45 setup_creator(&mut h, "projupdate").await;
46
47 let resp = h
48 .client
49 .post_form("/api/projects", "slug=updatable&title=Original")
50 .await;
51 let project: Value = resp.json();
52 let project_id = project["id"].as_str().unwrap();
53
54 let resp = h
55 .client
56 .put_json(
57 &format!("/api/projects/{}", project_id),
58 r#"{"title": "Updated Title", "description": "A new description"}"#,
59 )
60 .await;
61 assert!(resp.status.is_success(), "Update project failed: {} {}", resp.status, resp.text);
62
63 // Verify in DB
64 let (title, desc): (String, Option<String>) = sqlx::query_as(
65 "SELECT title, description FROM projects WHERE id = $1::uuid",
66 )
67 .bind(project_id)
68 .fetch_one(&h.db)
69 .await
70 .unwrap();
71 assert_eq!(title, "Updated Title");
72 assert_eq!(desc.as_deref(), Some("A new description"));
73 }
74
75 #[tokio::test]
76 async fn update_project_non_owner_rejected() {
77 let mut h = TestHarness::new().await;
78 setup_creator(&mut h, "projown").await;
79
80 let resp = h
81 .client
82 .post_form("/api/projects", "slug=owned-proj&title=Owned")
83 .await;
84 let project: Value = resp.json();
85 let project_id = project["id"].as_str().unwrap().to_string();
86
87 // Switch to different creator
88 h.client.post_form("/logout", "").await;
89 setup_creator(&mut h, "projintruder").await;
90
91 let resp = h
92 .client
93 .put_json(
94 &format!("/api/projects/{}", project_id),
95 r#"{"title": "Hacked"}"#,
96 )
97 .await;
98 assert_eq!(resp.status, 403, "Non-owner update should be 403: {}", resp.text);
99 }
100
101 #[tokio::test]
102 async fn delete_project_cascades_to_items() {
103 let mut h = TestHarness::new().await;
104 setup_creator(&mut h, "projdel").await;
105
106 let resp = h
107 .client
108 .post_form("/api/projects", "slug=deleteme&title=Delete+Me")
109 .await;
110 let project: Value = resp.json();
111 let project_id = project["id"].as_str().unwrap().to_string();
112
113 // Add items
114 for i in 0..3 {
115 let resp = h
116 .client
117 .post_form(
118 &format!("/api/projects/{}/items", project_id),
119 &format!("title=Item+{}", i),
120 )
121 .await;
122 assert!(resp.status.is_success());
123 }
124
125 // Delete project
126 let resp = h
127 .client
128 .delete(&format!("/api/projects/{}", project_id))
129 .await;
130 assert!(resp.status.is_success(), "Delete project failed: {} {}", resp.status, resp.text);
131
132 // Verify project and items are gone
133 let proj_count: i64 =
134 sqlx::query_scalar("SELECT COUNT(*) FROM projects WHERE id = $1::uuid")
135 .bind(&project_id)
136 .fetch_one(&h.db)
137 .await
138 .unwrap();
139 assert_eq!(proj_count, 0, "Project should be deleted");
140
141 let item_count: i64 =
142 sqlx::query_scalar("SELECT COUNT(*) FROM items WHERE project_id = $1::uuid")
143 .bind(&project_id)
144 .fetch_one(&h.db)
145 .await
146 .unwrap();
147 assert_eq!(item_count, 0, "Items should be cascade-deleted");
148 }
149
150 #[tokio::test]
151 async fn delete_project_non_owner_rejected() {
152 let mut h = TestHarness::new().await;
153 setup_creator(&mut h, "projdelown").await;
154
155 let resp = h
156 .client
157 .post_form("/api/projects", "slug=nodelete&title=No+Delete")
158 .await;
159 let project: Value = resp.json();
160 let project_id = project["id"].as_str().unwrap().to_string();
161
162 // Switch user
163 h.client.post_form("/logout", "").await;
164 setup_creator(&mut h, "projdelother").await;
165
166 let resp = h.client.delete(&format!("/api/projects/{}", project_id)).await;
167 assert_eq!(resp.status, 403, "Non-owner delete should be 403: {}", resp.text);
168 }
169
170 #[tokio::test]
171 async fn duplicate_slug_rejected() {
172 let mut h = TestHarness::new().await;
173 setup_creator(&mut h, "projslug").await;
174
175 let resp = h
176 .client
177 .post_form("/api/projects", "slug=unique-slug&title=First")
178 .await;
179 assert!(resp.status.is_success());
180
181 // Same slug should fail (may return 4xx or 5xx depending on error handling)
182 let resp = h
183 .client
184 .post_form("/api/projects", "slug=unique-slug&title=Second")
185 .await;
186 assert!(
187 !resp.status.is_success(),
188 "Duplicate slug should be rejected: {} {}",
189 resp.status, resp.text
190 );
191 }
192
193 #[tokio::test]
194 async fn project_with_category() {
195 let mut h = TestHarness::new().await;
196 setup_creator(&mut h, "projcat").await;
197
198 let resp = h
199 .client
200 .post_form(
201 "/api/projects",
202 "slug=music-proj&title=Music+Project&category=Music",
203 )
204 .await;
205 assert!(resp.status.is_success(), "Create project with category failed: {}", resp.text);
206
207 // Verify category was set
208 let category: Option<String> =
209 sqlx::query_scalar("SELECT c.name FROM projects p JOIN project_categories c ON p.category_id = c.id WHERE p.slug = 'music-proj'")
210 .fetch_optional(&h.db)
211 .await
212 .unwrap();
213 assert_eq!(category.as_deref(), Some("Music"), "Category should be set");
214 }
215