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