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