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")
958 .await
959 .unwrap();
960 let (user2, inviter, _) = db
961 .create_user_from_invite(
962 &user2_invite,
963 NewUserParams {
964 github_login: "user2".into(),
965 github_user_id: 2,
966 invite_count: 7,
967 },
968 )
969 .await
970 .unwrap();
971 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
972 assert_eq!(invite_count, 1);
973 assert_eq!(inviter, Some(user1));
974 assert_eq!(
975 db.get_contacts(user1).await.unwrap(),
976 [
977 Contact::Accepted {
978 user_id: user1,
979 should_notify: false
980 },
981 Contact::Accepted {
982 user_id: user2,
983 should_notify: true
984 }
985 ]
986 );
987 assert_eq!(
988 db.get_contacts(user2).await.unwrap(),
989 [
990 Contact::Accepted {
991 user_id: user1,
992 should_notify: false
993 },
994 Contact::Accepted {
995 user_id: user2,
996 should_notify: false
997 }
998 ]
999 );
1000 assert_eq!(
1001 db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
1002 7
1003 );
1004
1005 // User 3 redeems the invite code and becomes a contact of user 1.
1006 let user3_invite = db
1007 .create_invite_from_code(&invite_code, "u3@example.com")
1008 .await
1009 .unwrap();
1010 let (user3, inviter, _) = db
1011 .create_user_from_invite(
1012 &user3_invite,
1013 NewUserParams {
1014 github_login: "user-3".into(),
1015 github_user_id: 3,
1016 invite_count: 3,
1017 },
1018 )
1019 .await
1020 .unwrap();
1021 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1022 assert_eq!(invite_count, 0);
1023 assert_eq!(inviter, Some(user1));
1024 assert_eq!(
1025 db.get_contacts(user1).await.unwrap(),
1026 [
1027 Contact::Accepted {
1028 user_id: user1,
1029 should_notify: false
1030 },
1031 Contact::Accepted {
1032 user_id: user2,
1033 should_notify: true
1034 },
1035 Contact::Accepted {
1036 user_id: user3,
1037 should_notify: true
1038 }
1039 ]
1040 );
1041 assert_eq!(
1042 db.get_contacts(user3).await.unwrap(),
1043 [
1044 Contact::Accepted {
1045 user_id: user1,
1046 should_notify: false
1047 },
1048 Contact::Accepted {
1049 user_id: user3,
1050 should_notify: false
1051 },
1052 ]
1053 );
1054 assert_eq!(
1055 db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
1056 3
1057 );
1058
1059 // Trying to reedem the code for the third time results in an error.
1060 db.create_invite_from_code(&invite_code, "u4@example.com")
1061 .await
1062 .unwrap_err();
1063
1064 // Invite count can be updated after the code has been created.
1065 db.set_invite_count_for_user(user1, 2).await.unwrap();
1066 let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1067 assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
1068 assert_eq!(invite_count, 2);
1069
1070 // User 4 can now redeem the invite code and becomes a contact of user 1.
1071 let user4_invite = db
1072 .create_invite_from_code(&invite_code, "u4@example.com")
1073 .await
1074 .unwrap();
1075 let (user4, _, _) = db
1076 .create_user_from_invite(
1077 &user4_invite,
1078 NewUserParams {
1079 github_login: "user-4".into(),
1080 github_user_id: 4,
1081 invite_count: 5,
1082 },
1083 )
1084 .await
1085 .unwrap();
1086
1087 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1088 assert_eq!(invite_count, 1);
1089 assert_eq!(
1090 db.get_contacts(user1).await.unwrap(),
1091 [
1092 Contact::Accepted {
1093 user_id: user1,
1094 should_notify: false
1095 },
1096 Contact::Accepted {
1097 user_id: user2,
1098 should_notify: true
1099 },
1100 Contact::Accepted {
1101 user_id: user3,
1102 should_notify: true
1103 },
1104 Contact::Accepted {
1105 user_id: user4,
1106 should_notify: true
1107 }
1108 ]
1109 );
1110 assert_eq!(
1111 db.get_contacts(user4).await.unwrap(),
1112 [
1113 Contact::Accepted {
1114 user_id: user1,
1115 should_notify: false
1116 },
1117 Contact::Accepted {
1118 user_id: user4,
1119 should_notify: false
1120 },
1121 ]
1122 );
1123 assert_eq!(
1124 db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
1125 5
1126 );
1127
1128 // An existing user cannot redeem invite codes.
1129 db.create_invite_from_code(&invite_code, "u2@example.com")
1130 .await
1131 .unwrap_err();
1132 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1133 assert_eq!(invite_count, 1);
1134}
1135
1136#[tokio::test(flavor = "multi_thread")]
1137async fn test_signups() {
1138 let postgres = TestDb::postgres().await;
1139 let db = postgres.db();
1140
1141 // people sign up on the waitlist
1142 for i in 0..8 {
1143 db.create_signup(Signup {
1144 email_address: format!("person-{i}@example.com"),
1145 platform_mac: true,
1146 platform_linux: i % 2 == 0,
1147 platform_windows: i % 4 == 0,
1148 editor_features: vec!["speed".into()],
1149 programming_languages: vec!["rust".into(), "c".into()],
1150 device_id: format!("device_id_{i}"),
1151 })
1152 .await
1153 .unwrap();
1154 }
1155
1156 assert_eq!(
1157 db.get_waitlist_summary().await.unwrap(),
1158 WaitlistSummary {
1159 count: 8,
1160 mac_count: 8,
1161 linux_count: 4,
1162 windows_count: 2,
1163 }
1164 );
1165
1166 // retrieve the next batch of signup emails to send
1167 let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
1168 let addresses = signups_batch1
1169 .iter()
1170 .map(|s| &s.email_address)
1171 .collect::<Vec<_>>();
1172 assert_eq!(
1173 addresses,
1174 &[
1175 "person-0@example.com",
1176 "person-1@example.com",
1177 "person-2@example.com"
1178 ]
1179 );
1180 assert_ne!(
1181 signups_batch1[0].email_confirmation_code,
1182 signups_batch1[1].email_confirmation_code
1183 );
1184
1185 // the waitlist isn't updated until we record that the emails
1186 // were successfully sent.
1187 let signups_batch = db.get_unsent_invites(3).await.unwrap();
1188 assert_eq!(signups_batch, signups_batch1);
1189
1190 // once the emails go out, we can retrieve the next batch
1191 // of signups.
1192 db.record_sent_invites(&signups_batch1).await.unwrap();
1193 let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
1194 let addresses = signups_batch2
1195 .iter()
1196 .map(|s| &s.email_address)
1197 .collect::<Vec<_>>();
1198 assert_eq!(
1199 addresses,
1200 &[
1201 "person-3@example.com",
1202 "person-4@example.com",
1203 "person-5@example.com"
1204 ]
1205 );
1206
1207 // the sent invites are excluded from the summary.
1208 assert_eq!(
1209 db.get_waitlist_summary().await.unwrap(),
1210 WaitlistSummary {
1211 count: 5,
1212 mac_count: 5,
1213 linux_count: 2,
1214 windows_count: 1,
1215 }
1216 );
1217
1218 // user completes the signup process by providing their
1219 // github account.
1220 let (user_id, inviter_id, signup_device_id) = db
1221 .create_user_from_invite(
1222 &Invite {
1223 email_address: signups_batch1[0].email_address.clone(),
1224 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1225 },
1226 NewUserParams {
1227 github_login: "person-0".into(),
1228 github_user_id: 0,
1229 invite_count: 5,
1230 },
1231 )
1232 .await
1233 .unwrap();
1234 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1235 assert!(inviter_id.is_none());
1236 assert_eq!(user.github_login, "person-0");
1237 assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
1238 assert_eq!(user.invite_count, 5);
1239 assert_eq!(signup_device_id, "device_id_0");
1240
1241 // cannot redeem the same signup again.
1242 db.create_user_from_invite(
1243 &Invite {
1244 email_address: signups_batch1[0].email_address.clone(),
1245 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1246 },
1247 NewUserParams {
1248 github_login: "some-other-github_account".into(),
1249 github_user_id: 1,
1250 invite_count: 5,
1251 },
1252 )
1253 .await
1254 .unwrap_err();
1255
1256 // cannot redeem a signup with the wrong confirmation code.
1257 db.create_user_from_invite(
1258 &Invite {
1259 email_address: signups_batch1[1].email_address.clone(),
1260 email_confirmation_code: "the-wrong-code".to_string(),
1261 },
1262 NewUserParams {
1263 github_login: "person-1".into(),
1264 github_user_id: 2,
1265 invite_count: 5,
1266 },
1267 )
1268 .await
1269 .unwrap_err();
1270}
1271
1272fn build_background_executor() -> Arc<Background> {
1273 Deterministic::new(0).build_background()
1274}