1use super::db::*;
2use collections::HashMap;
3use gpui::executor::{Background, Deterministic};
4use std::{sync::Arc, time::Duration};
5use time::OffsetDateTime;
6
7#[tokio::test(flavor = "multi_thread")]
8async fn test_get_users_by_ids() {
9 for test_db in [
10 TestDb::postgres().await,
11 TestDb::fake(build_background_executor()),
12 ] {
13 let db = test_db.db();
14
15 let user1 = db.create_user("u1", "u1@example.com", false).await.unwrap();
16 let user2 = db.create_user("u2", "u2@example.com", false).await.unwrap();
17 let user3 = db.create_user("u3", "u3@example.com", false).await.unwrap();
18 let user4 = db.create_user("u4", "u4@example.com", false).await.unwrap();
19
20 assert_eq!(
21 db.get_users_by_ids(vec![user1, user2, user3, user4])
22 .await
23 .unwrap(),
24 vec![
25 User {
26 id: user1,
27 github_login: "u1".to_string(),
28 email_address: Some("u1@example.com".to_string()),
29 admin: false,
30 ..Default::default()
31 },
32 User {
33 id: user2,
34 github_login: "u2".to_string(),
35 email_address: Some("u2@example.com".to_string()),
36 admin: false,
37 ..Default::default()
38 },
39 User {
40 id: user3,
41 github_login: "u3".to_string(),
42 email_address: Some("u3@example.com".to_string()),
43 admin: false,
44 ..Default::default()
45 },
46 User {
47 id: user4,
48 github_login: "u4".to_string(),
49 email_address: Some("u4@example.com".to_string()),
50 admin: false,
51 ..Default::default()
52 }
53 ]
54 );
55 }
56}
57
58#[tokio::test(flavor = "multi_thread")]
59async fn test_create_users() {
60 let db = TestDb::postgres().await;
61 let db = db.db();
62
63 // Create the first batch of users, ensuring invite counts are assigned
64 // correctly and the respective invite codes are unique.
65 let user_ids_batch_1 = db
66 .create_users(vec![
67 ("user1".to_string(), "hi@user1.com".to_string(), 5),
68 ("user2".to_string(), "hi@user2.com".to_string(), 4),
69 ("user3".to_string(), "hi@user3.com".to_string(), 3),
70 ])
71 .await
72 .unwrap();
73 assert_eq!(user_ids_batch_1.len(), 3);
74
75 let users = db.get_users_by_ids(user_ids_batch_1.clone()).await.unwrap();
76 assert_eq!(users.len(), 3);
77 assert_eq!(users[0].github_login, "user1");
78 assert_eq!(users[0].email_address.as_deref(), Some("hi@user1.com"));
79 assert_eq!(users[0].invite_count, 5);
80 assert_eq!(users[1].github_login, "user2");
81 assert_eq!(users[1].email_address.as_deref(), Some("hi@user2.com"));
82 assert_eq!(users[1].invite_count, 4);
83 assert_eq!(users[2].github_login, "user3");
84 assert_eq!(users[2].email_address.as_deref(), Some("hi@user3.com"));
85 assert_eq!(users[2].invite_count, 3);
86
87 let invite_code_1 = users[0].invite_code.clone().unwrap();
88 let invite_code_2 = users[1].invite_code.clone().unwrap();
89 let invite_code_3 = users[2].invite_code.clone().unwrap();
90 assert_ne!(invite_code_1, invite_code_2);
91 assert_ne!(invite_code_1, invite_code_3);
92 assert_ne!(invite_code_2, invite_code_3);
93
94 // Create the second batch of users and include a user that is already in the database, ensuring
95 // the invite count for the existing user is updated without changing their invite code.
96 let user_ids_batch_2 = db
97 .create_users(vec![
98 ("user2".to_string(), "hi@user2.com".to_string(), 10),
99 ("user4".to_string(), "hi@user4.com".to_string(), 2),
100 ])
101 .await
102 .unwrap();
103 assert_eq!(user_ids_batch_2.len(), 2);
104 assert_eq!(user_ids_batch_2[0], user_ids_batch_1[1]);
105
106 let users = db.get_users_by_ids(user_ids_batch_2).await.unwrap();
107 assert_eq!(users.len(), 2);
108 assert_eq!(users[0].github_login, "user2");
109 assert_eq!(users[0].email_address.as_deref(), Some("hi@user2.com"));
110 assert_eq!(users[0].invite_count, 10);
111 assert_eq!(users[0].invite_code, Some(invite_code_2.clone()));
112 assert_eq!(users[1].github_login, "user4");
113 assert_eq!(users[1].email_address.as_deref(), Some("hi@user4.com"));
114 assert_eq!(users[1].invite_count, 2);
115
116 let invite_code_4 = users[1].invite_code.clone().unwrap();
117 assert_ne!(invite_code_4, invite_code_1);
118 assert_ne!(invite_code_4, invite_code_2);
119 assert_ne!(invite_code_4, invite_code_3);
120}
121
122#[tokio::test(flavor = "multi_thread")]
123async fn test_worktree_extensions() {
124 let test_db = TestDb::postgres().await;
125 let db = test_db.db();
126
127 let user = db.create_user("u1", "u1@example.com", false).await.unwrap();
128 let project = db.register_project(user).await.unwrap();
129
130 db.update_worktree_extensions(project, 100, Default::default())
131 .await
132 .unwrap();
133 db.update_worktree_extensions(
134 project,
135 100,
136 [("rs".to_string(), 5), ("md".to_string(), 3)]
137 .into_iter()
138 .collect(),
139 )
140 .await
141 .unwrap();
142 db.update_worktree_extensions(
143 project,
144 100,
145 [("rs".to_string(), 6), ("md".to_string(), 5)]
146 .into_iter()
147 .collect(),
148 )
149 .await
150 .unwrap();
151 db.update_worktree_extensions(
152 project,
153 101,
154 [("ts".to_string(), 2), ("md".to_string(), 1)]
155 .into_iter()
156 .collect(),
157 )
158 .await
159 .unwrap();
160
161 assert_eq!(
162 db.get_project_extensions(project).await.unwrap(),
163 [
164 (
165 100,
166 [("rs".into(), 6), ("md".into(), 5),]
167 .into_iter()
168 .collect::<HashMap<_, _>>()
169 ),
170 (
171 101,
172 [("ts".into(), 2), ("md".into(), 1),]
173 .into_iter()
174 .collect::<HashMap<_, _>>()
175 )
176 ]
177 .into_iter()
178 .collect()
179 );
180}
181
182#[tokio::test(flavor = "multi_thread")]
183async fn test_user_activity() {
184 let test_db = TestDb::postgres().await;
185 let db = test_db.db();
186
187 let user_1 = db.create_user("u1", "u1@example.com", false).await.unwrap();
188 let user_2 = db.create_user("u2", "u2@example.com", false).await.unwrap();
189 let user_3 = db.create_user("u3", "u3@example.com", false).await.unwrap();
190 let project_1 = db.register_project(user_1).await.unwrap();
191 db.update_worktree_extensions(
192 project_1,
193 1,
194 HashMap::from_iter([("rs".into(), 5), ("md".into(), 7)]),
195 )
196 .await
197 .unwrap();
198 let project_2 = db.register_project(user_2).await.unwrap();
199 let t0 = OffsetDateTime::now_utc() - Duration::from_secs(60 * 60);
200
201 // User 2 opens a project
202 let t1 = t0 + Duration::from_secs(10);
203 db.record_user_activity(t0..t1, &[(user_2, project_2)])
204 .await
205 .unwrap();
206
207 let t2 = t1 + Duration::from_secs(10);
208 db.record_user_activity(t1..t2, &[(user_2, project_2)])
209 .await
210 .unwrap();
211
212 // User 1 joins the project
213 let t3 = t2 + Duration::from_secs(10);
214 db.record_user_activity(t2..t3, &[(user_2, project_2), (user_1, project_2)])
215 .await
216 .unwrap();
217
218 // User 1 opens another project
219 let t4 = t3 + Duration::from_secs(10);
220 db.record_user_activity(
221 t3..t4,
222 &[
223 (user_2, project_2),
224 (user_1, project_2),
225 (user_1, project_1),
226 ],
227 )
228 .await
229 .unwrap();
230
231 // User 3 joins that project
232 let t5 = t4 + Duration::from_secs(10);
233 db.record_user_activity(
234 t4..t5,
235 &[
236 (user_2, project_2),
237 (user_1, project_2),
238 (user_1, project_1),
239 (user_3, project_1),
240 ],
241 )
242 .await
243 .unwrap();
244
245 // User 2 leaves
246 let t6 = t5 + Duration::from_secs(5);
247 db.record_user_activity(t5..t6, &[(user_1, project_1), (user_3, project_1)])
248 .await
249 .unwrap();
250
251 let t7 = t6 + Duration::from_secs(60);
252 let t8 = t7 + Duration::from_secs(10);
253 db.record_user_activity(t7..t8, &[(user_1, project_1)])
254 .await
255 .unwrap();
256
257 assert_eq!(
258 db.get_top_users_activity_summary(t0..t6, 10).await.unwrap(),
259 &[
260 UserActivitySummary {
261 id: user_1,
262 github_login: "u1".to_string(),
263 project_activity: vec![
264 ProjectActivitySummary {
265 id: project_1,
266 duration: Duration::from_secs(25),
267 max_collaborators: 2
268 },
269 ProjectActivitySummary {
270 id: project_2,
271 duration: Duration::from_secs(30),
272 max_collaborators: 2
273 }
274 ]
275 },
276 UserActivitySummary {
277 id: user_2,
278 github_login: "u2".to_string(),
279 project_activity: vec![ProjectActivitySummary {
280 id: project_2,
281 duration: Duration::from_secs(50),
282 max_collaborators: 2
283 }]
284 },
285 UserActivitySummary {
286 id: user_3,
287 github_login: "u3".to_string(),
288 project_activity: vec![ProjectActivitySummary {
289 id: project_1,
290 duration: Duration::from_secs(15),
291 max_collaborators: 2
292 }]
293 },
294 ]
295 );
296
297 assert_eq!(
298 db.get_active_user_count(t0..t6, Duration::from_secs(56), false)
299 .await
300 .unwrap(),
301 0
302 );
303 assert_eq!(
304 db.get_active_user_count(t0..t6, Duration::from_secs(56), true)
305 .await
306 .unwrap(),
307 0
308 );
309 assert_eq!(
310 db.get_active_user_count(t0..t6, Duration::from_secs(54), false)
311 .await
312 .unwrap(),
313 1
314 );
315 assert_eq!(
316 db.get_active_user_count(t0..t6, Duration::from_secs(54), true)
317 .await
318 .unwrap(),
319 1
320 );
321 assert_eq!(
322 db.get_active_user_count(t0..t6, Duration::from_secs(30), false)
323 .await
324 .unwrap(),
325 2
326 );
327 assert_eq!(
328 db.get_active_user_count(t0..t6, Duration::from_secs(30), true)
329 .await
330 .unwrap(),
331 2
332 );
333 assert_eq!(
334 db.get_active_user_count(t0..t6, Duration::from_secs(10), false)
335 .await
336 .unwrap(),
337 3
338 );
339 assert_eq!(
340 db.get_active_user_count(t0..t6, Duration::from_secs(10), true)
341 .await
342 .unwrap(),
343 3
344 );
345 assert_eq!(
346 db.get_active_user_count(t0..t1, Duration::from_secs(5), false)
347 .await
348 .unwrap(),
349 1
350 );
351 assert_eq!(
352 db.get_active_user_count(t0..t1, Duration::from_secs(5), true)
353 .await
354 .unwrap(),
355 0
356 );
357
358 assert_eq!(
359 db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(),
360 &[
361 UserActivityPeriod {
362 project_id: project_1,
363 start: t3,
364 end: t6,
365 extensions: HashMap::from_iter([("rs".to_string(), 5), ("md".to_string(), 7)]),
366 },
367 UserActivityPeriod {
368 project_id: project_2,
369 start: t3,
370 end: t5,
371 extensions: Default::default(),
372 },
373 ]
374 );
375 assert_eq!(
376 db.get_user_activity_timeline(t0..t8, user_1).await.unwrap(),
377 &[
378 UserActivityPeriod {
379 project_id: project_2,
380 start: t2,
381 end: t5,
382 extensions: Default::default(),
383 },
384 UserActivityPeriod {
385 project_id: project_1,
386 start: t3,
387 end: t6,
388 extensions: HashMap::from_iter([("rs".to_string(), 5), ("md".to_string(), 7)]),
389 },
390 UserActivityPeriod {
391 project_id: project_1,
392 start: t7,
393 end: t8,
394 extensions: HashMap::from_iter([("rs".to_string(), 5), ("md".to_string(), 7)]),
395 },
396 ]
397 );
398}
399
400#[tokio::test(flavor = "multi_thread")]
401async fn test_recent_channel_messages() {
402 for test_db in [
403 TestDb::postgres().await,
404 TestDb::fake(build_background_executor()),
405 ] {
406 let db = test_db.db();
407 let user = db.create_user("u", "u@example.com", false).await.unwrap();
408 let org = db.create_org("org", "org").await.unwrap();
409 let channel = db.create_org_channel(org, "channel").await.unwrap();
410 for i in 0..10 {
411 db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
412 .await
413 .unwrap();
414 }
415
416 let messages = db.get_channel_messages(channel, 5, None).await.unwrap();
417 assert_eq!(
418 messages.iter().map(|m| &m.body).collect::<Vec<_>>(),
419 ["5", "6", "7", "8", "9"]
420 );
421
422 let prev_messages = db
423 .get_channel_messages(channel, 4, Some(messages[0].id))
424 .await
425 .unwrap();
426 assert_eq!(
427 prev_messages.iter().map(|m| &m.body).collect::<Vec<_>>(),
428 ["1", "2", "3", "4"]
429 );
430 }
431}
432
433#[tokio::test(flavor = "multi_thread")]
434async fn test_channel_message_nonces() {
435 for test_db in [
436 TestDb::postgres().await,
437 TestDb::fake(build_background_executor()),
438 ] {
439 let db = test_db.db();
440 let user = db.create_user("u", "u@example.com", false).await.unwrap();
441 let org = db.create_org("org", "org").await.unwrap();
442 let channel = db.create_org_channel(org, "channel").await.unwrap();
443
444 let msg1_id = db
445 .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
446 .await
447 .unwrap();
448 let msg2_id = db
449 .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
450 .await
451 .unwrap();
452 let msg3_id = db
453 .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
454 .await
455 .unwrap();
456 let msg4_id = db
457 .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
458 .await
459 .unwrap();
460
461 assert_ne!(msg1_id, msg2_id);
462 assert_eq!(msg1_id, msg3_id);
463 assert_eq!(msg2_id, msg4_id);
464 }
465}
466
467#[tokio::test(flavor = "multi_thread")]
468async fn test_create_access_tokens() {
469 let test_db = TestDb::postgres().await;
470 let db = test_db.db();
471 let user = db.create_user("u1", "u1@example.com", false).await.unwrap();
472
473 db.create_access_token_hash(user, "h1", 3).await.unwrap();
474 db.create_access_token_hash(user, "h2", 3).await.unwrap();
475 assert_eq!(
476 db.get_access_token_hashes(user).await.unwrap(),
477 &["h2".to_string(), "h1".to_string()]
478 );
479
480 db.create_access_token_hash(user, "h3", 3).await.unwrap();
481 assert_eq!(
482 db.get_access_token_hashes(user).await.unwrap(),
483 &["h3".to_string(), "h2".to_string(), "h1".to_string(),]
484 );
485
486 db.create_access_token_hash(user, "h4", 3).await.unwrap();
487 assert_eq!(
488 db.get_access_token_hashes(user).await.unwrap(),
489 &["h4".to_string(), "h3".to_string(), "h2".to_string(),]
490 );
491
492 db.create_access_token_hash(user, "h5", 3).await.unwrap();
493 assert_eq!(
494 db.get_access_token_hashes(user).await.unwrap(),
495 &["h5".to_string(), "h4".to_string(), "h3".to_string()]
496 );
497}
498
499#[test]
500fn test_fuzzy_like_string() {
501 assert_eq!(PostgresDb::fuzzy_like_string("abcd"), "%a%b%c%d%");
502 assert_eq!(PostgresDb::fuzzy_like_string("x y"), "%x%y%");
503 assert_eq!(PostgresDb::fuzzy_like_string(" z "), "%z%");
504}
505
506#[tokio::test(flavor = "multi_thread")]
507async fn test_fuzzy_search_users() {
508 let test_db = TestDb::postgres().await;
509 let db = test_db.db();
510 for github_login in [
511 "California",
512 "colorado",
513 "oregon",
514 "washington",
515 "florida",
516 "delaware",
517 "rhode-island",
518 ] {
519 db.create_user(github_login, &format!("{github_login}@example.com"), false)
520 .await
521 .unwrap();
522 }
523
524 assert_eq!(
525 fuzzy_search_user_names(db, "clr").await,
526 &["colorado", "California"]
527 );
528 assert_eq!(
529 fuzzy_search_user_names(db, "ro").await,
530 &["rhode-island", "colorado", "oregon"],
531 );
532
533 async fn fuzzy_search_user_names(db: &Arc<dyn Db>, query: &str) -> Vec<String> {
534 db.fuzzy_search_users(query, 10)
535 .await
536 .unwrap()
537 .into_iter()
538 .map(|user| user.github_login)
539 .collect::<Vec<_>>()
540 }
541}
542
543#[tokio::test(flavor = "multi_thread")]
544async fn test_add_contacts() {
545 for test_db in [
546 TestDb::postgres().await,
547 TestDb::fake(build_background_executor()),
548 ] {
549 let db = test_db.db();
550
551 let user_1 = db.create_user("u1", "u1@example.com", false).await.unwrap();
552 let user_2 = db.create_user("u2", "u2@example.com", false).await.unwrap();
553 let user_3 = db.create_user("u3", "u3@example.com", false).await.unwrap();
554
555 // User starts with no contacts
556 assert_eq!(
557 db.get_contacts(user_1).await.unwrap(),
558 vec![Contact::Accepted {
559 user_id: user_1,
560 should_notify: false
561 }],
562 );
563
564 // User requests a contact. Both users see the pending request.
565 db.send_contact_request(user_1, user_2).await.unwrap();
566 assert!(!db.has_contact(user_1, user_2).await.unwrap());
567 assert!(!db.has_contact(user_2, user_1).await.unwrap());
568 assert_eq!(
569 db.get_contacts(user_1).await.unwrap(),
570 &[
571 Contact::Accepted {
572 user_id: user_1,
573 should_notify: false
574 },
575 Contact::Outgoing { user_id: user_2 }
576 ],
577 );
578 assert_eq!(
579 db.get_contacts(user_2).await.unwrap(),
580 &[
581 Contact::Incoming {
582 user_id: user_1,
583 should_notify: true
584 },
585 Contact::Accepted {
586 user_id: user_2,
587 should_notify: false
588 },
589 ]
590 );
591
592 // User 2 dismisses the contact request notification without accepting or rejecting.
593 // We shouldn't notify them again.
594 db.dismiss_contact_notification(user_1, user_2)
595 .await
596 .unwrap_err();
597 db.dismiss_contact_notification(user_2, user_1)
598 .await
599 .unwrap();
600 assert_eq!(
601 db.get_contacts(user_2).await.unwrap(),
602 &[
603 Contact::Incoming {
604 user_id: user_1,
605 should_notify: false
606 },
607 Contact::Accepted {
608 user_id: user_2,
609 should_notify: false
610 },
611 ]
612 );
613
614 // User can't accept their own contact request
615 db.respond_to_contact_request(user_1, user_2, true)
616 .await
617 .unwrap_err();
618
619 // User accepts a contact request. Both users see the contact.
620 db.respond_to_contact_request(user_2, user_1, true)
621 .await
622 .unwrap();
623 assert_eq!(
624 db.get_contacts(user_1).await.unwrap(),
625 &[
626 Contact::Accepted {
627 user_id: user_1,
628 should_notify: false
629 },
630 Contact::Accepted {
631 user_id: user_2,
632 should_notify: true
633 }
634 ],
635 );
636 assert!(db.has_contact(user_1, user_2).await.unwrap());
637 assert!(db.has_contact(user_2, user_1).await.unwrap());
638 assert_eq!(
639 db.get_contacts(user_2).await.unwrap(),
640 &[
641 Contact::Accepted {
642 user_id: user_1,
643 should_notify: false,
644 },
645 Contact::Accepted {
646 user_id: user_2,
647 should_notify: false,
648 },
649 ]
650 );
651
652 // Users cannot re-request existing contacts.
653 db.send_contact_request(user_1, user_2).await.unwrap_err();
654 db.send_contact_request(user_2, user_1).await.unwrap_err();
655
656 // Users can't dismiss notifications of them accepting other users' requests.
657 db.dismiss_contact_notification(user_2, user_1)
658 .await
659 .unwrap_err();
660 assert_eq!(
661 db.get_contacts(user_1).await.unwrap(),
662 &[
663 Contact::Accepted {
664 user_id: user_1,
665 should_notify: false
666 },
667 Contact::Accepted {
668 user_id: user_2,
669 should_notify: true,
670 },
671 ]
672 );
673
674 // Users can dismiss notifications of other users accepting their requests.
675 db.dismiss_contact_notification(user_1, user_2)
676 .await
677 .unwrap();
678 assert_eq!(
679 db.get_contacts(user_1).await.unwrap(),
680 &[
681 Contact::Accepted {
682 user_id: user_1,
683 should_notify: false
684 },
685 Contact::Accepted {
686 user_id: user_2,
687 should_notify: false,
688 },
689 ]
690 );
691
692 // Users send each other concurrent contact requests and
693 // see that they are immediately accepted.
694 db.send_contact_request(user_1, user_3).await.unwrap();
695 db.send_contact_request(user_3, user_1).await.unwrap();
696 assert_eq!(
697 db.get_contacts(user_1).await.unwrap(),
698 &[
699 Contact::Accepted {
700 user_id: user_1,
701 should_notify: false
702 },
703 Contact::Accepted {
704 user_id: user_2,
705 should_notify: false,
706 },
707 Contact::Accepted {
708 user_id: user_3,
709 should_notify: false
710 },
711 ]
712 );
713 assert_eq!(
714 db.get_contacts(user_3).await.unwrap(),
715 &[
716 Contact::Accepted {
717 user_id: user_1,
718 should_notify: false
719 },
720 Contact::Accepted {
721 user_id: user_3,
722 should_notify: false
723 }
724 ],
725 );
726
727 // User declines a contact request. Both users see that it is gone.
728 db.send_contact_request(user_2, user_3).await.unwrap();
729 db.respond_to_contact_request(user_3, user_2, false)
730 .await
731 .unwrap();
732 assert!(!db.has_contact(user_2, user_3).await.unwrap());
733 assert!(!db.has_contact(user_3, user_2).await.unwrap());
734 assert_eq!(
735 db.get_contacts(user_2).await.unwrap(),
736 &[
737 Contact::Accepted {
738 user_id: user_1,
739 should_notify: false
740 },
741 Contact::Accepted {
742 user_id: user_2,
743 should_notify: false
744 }
745 ]
746 );
747 assert_eq!(
748 db.get_contacts(user_3).await.unwrap(),
749 &[
750 Contact::Accepted {
751 user_id: user_1,
752 should_notify: false
753 },
754 Contact::Accepted {
755 user_id: user_3,
756 should_notify: false
757 }
758 ],
759 );
760 }
761}
762
763#[tokio::test(flavor = "multi_thread")]
764async fn test_invite_codes() {
765 let postgres = TestDb::postgres().await;
766 let db = postgres.db();
767 let user1 = db.create_user("u1", "u1@example.com", false).await.unwrap();
768
769 // Initially, user 1 has no invite code
770 assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
771
772 // Setting invite count to 0 when no code is assigned does not assign a new code
773 db.set_invite_count_for_user(user1, 0).await.unwrap();
774 assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
775
776 // User 1 creates an invite code that can be used twice.
777 db.set_invite_count_for_user(user1, 2).await.unwrap();
778 let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
779 assert_eq!(invite_count, 2);
780
781 // User 2 redeems the invite code and becomes a contact of user 1.
782 let user2_invite = db
783 .create_invite_from_code(&invite_code, "u2@example.com")
784 .await
785 .unwrap();
786 let (user2, inviter) = db
787 .create_user_from_invite(
788 &user2_invite,
789 NewUserParams {
790 github_login: "user2".into(),
791 invite_count: 7,
792 },
793 )
794 .await
795 .unwrap();
796 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
797 assert_eq!(invite_count, 1);
798 assert_eq!(inviter, Some(user1));
799 assert_eq!(
800 db.get_contacts(user1).await.unwrap(),
801 [
802 Contact::Accepted {
803 user_id: user1,
804 should_notify: false
805 },
806 Contact::Accepted {
807 user_id: user2,
808 should_notify: true
809 }
810 ]
811 );
812 assert_eq!(
813 db.get_contacts(user2).await.unwrap(),
814 [
815 Contact::Accepted {
816 user_id: user1,
817 should_notify: false
818 },
819 Contact::Accepted {
820 user_id: user2,
821 should_notify: false
822 }
823 ]
824 );
825 assert_eq!(
826 db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
827 7
828 );
829
830 // User 3 redeems the invite code and becomes a contact of user 1.
831 let user3_invite = db
832 .create_invite_from_code(&invite_code, "u3@example.com")
833 .await
834 .unwrap();
835 let (user3, inviter) = db
836 .create_user_from_invite(
837 &user3_invite,
838 NewUserParams {
839 github_login: "user-3".into(),
840 invite_count: 3,
841 },
842 )
843 .await
844 .unwrap();
845 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
846 assert_eq!(invite_count, 0);
847 assert_eq!(inviter, Some(user1));
848 assert_eq!(
849 db.get_contacts(user1).await.unwrap(),
850 [
851 Contact::Accepted {
852 user_id: user1,
853 should_notify: false
854 },
855 Contact::Accepted {
856 user_id: user2,
857 should_notify: true
858 },
859 Contact::Accepted {
860 user_id: user3,
861 should_notify: true
862 }
863 ]
864 );
865 assert_eq!(
866 db.get_contacts(user3).await.unwrap(),
867 [
868 Contact::Accepted {
869 user_id: user1,
870 should_notify: false
871 },
872 Contact::Accepted {
873 user_id: user3,
874 should_notify: false
875 },
876 ]
877 );
878 assert_eq!(
879 db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
880 3
881 );
882
883 // Trying to reedem the code for the third time results in an error.
884 db.create_invite_from_code(&invite_code, "u4@example.com")
885 .await
886 .unwrap_err();
887
888 // Invite count can be updated after the code has been created.
889 db.set_invite_count_for_user(user1, 2).await.unwrap();
890 let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
891 assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
892 assert_eq!(invite_count, 2);
893
894 // User 4 can now redeem the invite code and becomes a contact of user 1.
895 let user4_invite = db
896 .create_invite_from_code(&invite_code, "u4@example.com")
897 .await
898 .unwrap();
899 let (user4, _) = db
900 .create_user_from_invite(
901 &user4_invite,
902 NewUserParams {
903 github_login: "user-4".into(),
904 invite_count: 5,
905 },
906 )
907 .await
908 .unwrap();
909
910 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
911 assert_eq!(invite_count, 1);
912 assert_eq!(
913 db.get_contacts(user1).await.unwrap(),
914 [
915 Contact::Accepted {
916 user_id: user1,
917 should_notify: false
918 },
919 Contact::Accepted {
920 user_id: user2,
921 should_notify: true
922 },
923 Contact::Accepted {
924 user_id: user3,
925 should_notify: true
926 },
927 Contact::Accepted {
928 user_id: user4,
929 should_notify: true
930 }
931 ]
932 );
933 assert_eq!(
934 db.get_contacts(user4).await.unwrap(),
935 [
936 Contact::Accepted {
937 user_id: user1,
938 should_notify: false
939 },
940 Contact::Accepted {
941 user_id: user4,
942 should_notify: false
943 },
944 ]
945 );
946 assert_eq!(
947 db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
948 5
949 );
950
951 // An existing user cannot redeem invite codes.
952 db.create_invite_from_code(&invite_code, "u2@example.com")
953 .await
954 .unwrap_err();
955 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
956 assert_eq!(invite_count, 1);
957}
958
959#[tokio::test(flavor = "multi_thread")]
960async fn test_signups() {
961 let postgres = TestDb::postgres().await;
962 let db = postgres.db();
963
964 // people sign up on the waitlist
965 for i in 0..8 {
966 db.create_signup(Signup {
967 email_address: format!("person-{i}@example.com"),
968 platform_mac: true,
969 platform_linux: i % 2 == 0,
970 platform_windows: i % 4 == 0,
971 editor_features: vec!["speed".into()],
972 programming_languages: vec!["rust".into(), "c".into()],
973 })
974 .await
975 .unwrap();
976 }
977
978 assert_eq!(
979 db.get_waitlist_summary().await.unwrap(),
980 WaitlistSummary {
981 count: 8,
982 mac_count: 8,
983 linux_count: 4,
984 windows_count: 2,
985 }
986 );
987
988 // retrieve the next batch of signup emails to send
989 let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
990 let addresses = signups_batch1
991 .iter()
992 .map(|s| &s.email_address)
993 .collect::<Vec<_>>();
994 assert_eq!(
995 addresses,
996 &[
997 "person-0@example.com",
998 "person-1@example.com",
999 "person-2@example.com"
1000 ]
1001 );
1002 assert_ne!(
1003 signups_batch1[0].email_confirmation_code,
1004 signups_batch1[1].email_confirmation_code
1005 );
1006
1007 // the waitlist isn't updated until we record that the emails
1008 // were successfully sent.
1009 let signups_batch = db.get_unsent_invites(3).await.unwrap();
1010 assert_eq!(signups_batch, signups_batch1);
1011
1012 // once the emails go out, we can retrieve the next batch
1013 // of signups.
1014 db.record_sent_invites(&signups_batch1).await.unwrap();
1015 let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
1016 let addresses = signups_batch2
1017 .iter()
1018 .map(|s| &s.email_address)
1019 .collect::<Vec<_>>();
1020 assert_eq!(
1021 addresses,
1022 &[
1023 "person-3@example.com",
1024 "person-4@example.com",
1025 "person-5@example.com"
1026 ]
1027 );
1028
1029 // the sent invites are excluded from the summary.
1030 assert_eq!(
1031 db.get_waitlist_summary().await.unwrap(),
1032 WaitlistSummary {
1033 count: 5,
1034 mac_count: 5,
1035 linux_count: 2,
1036 windows_count: 1,
1037 }
1038 );
1039
1040 // user completes the signup process by providing their
1041 // github account.
1042 let (user_id, inviter_id) = db
1043 .create_user_from_invite(
1044 &Invite {
1045 email_address: signups_batch1[0].email_address.clone(),
1046 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1047 },
1048 NewUserParams {
1049 github_login: "person-0".into(),
1050 invite_count: 5,
1051 },
1052 )
1053 .await
1054 .unwrap();
1055 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1056 assert!(inviter_id.is_none());
1057 assert_eq!(user.github_login, "person-0");
1058 assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
1059 assert_eq!(user.invite_count, 5);
1060
1061 // cannot redeem the same signup again.
1062 db.create_user_from_invite(
1063 &Invite {
1064 email_address: signups_batch1[0].email_address.clone(),
1065 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1066 },
1067 NewUserParams {
1068 github_login: "some-other-github_account".into(),
1069 invite_count: 5,
1070 },
1071 )
1072 .await
1073 .unwrap_err();
1074
1075 // cannot redeem a signup with the wrong confirmation code.
1076 db.create_user_from_invite(
1077 &Invite {
1078 email_address: signups_batch1[1].email_address.clone(),
1079 email_confirmation_code: "the-wrong-code".to_string(),
1080 },
1081 NewUserParams {
1082 github_login: "person-1".into(),
1083 invite_count: 5,
1084 },
1085 )
1086 .await
1087 .unwrap_err();
1088}
1089
1090fn build_background_executor() -> Arc<Background> {
1091 Deterministic::new(0).build_background()
1092}