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