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 owner_id = db.create_server("test").await.unwrap().0 as u32;
414
415 let user1 = db
416 .create_user(
417 &format!("admin@example.com"),
418 true,
419 NewUserParams {
420 github_login: "admin".into(),
421 github_user_id: 0,
422 invite_count: 0,
423 },
424 )
425 .await
426 .unwrap();
427 let user2 = db
428 .create_user(
429 &format!("user@example.com"),
430 false,
431 NewUserParams {
432 github_login: "user".into(),
433 github_user_id: 1,
434 invite_count: 0,
435 },
436 )
437 .await
438 .unwrap();
439
440 let room_id = RoomId::from_proto(
441 db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
442 .await
443 .unwrap()
444 .id,
445 );
446 db.call(
447 room_id,
448 user1.user_id,
449 ConnectionId { owner_id, id: 0 },
450 user2.user_id,
451 None,
452 )
453 .await
454 .unwrap();
455 db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
456 .await
457 .unwrap();
458 assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
459
460 db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
461 .await
462 .unwrap();
463 assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
464
465 db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
466 .await
467 .unwrap();
468 assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
469
470 // Projects shared by admins aren't counted.
471 db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
472 .await
473 .unwrap();
474 assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
475
476 db.leave_room(ConnectionId { owner_id, id: 1 })
477 .await
478 .unwrap();
479 assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
480 }
481);
482
483#[test]
484fn test_fuzzy_like_string() {
485 assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%");
486 assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%");
487 assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
488}
489
490#[gpui::test]
491async fn test_fuzzy_search_users() {
492 let test_db = TestDb::postgres(build_background_executor());
493 let db = test_db.db();
494 for (i, github_login) in [
495 "California",
496 "colorado",
497 "oregon",
498 "washington",
499 "florida",
500 "delaware",
501 "rhode-island",
502 ]
503 .into_iter()
504 .enumerate()
505 {
506 db.create_user(
507 &format!("{github_login}@example.com"),
508 false,
509 NewUserParams {
510 github_login: github_login.into(),
511 github_user_id: i as i32,
512 invite_count: 0,
513 },
514 )
515 .await
516 .unwrap();
517 }
518
519 assert_eq!(
520 fuzzy_search_user_names(db, "clr").await,
521 &["colorado", "California"]
522 );
523 assert_eq!(
524 fuzzy_search_user_names(db, "ro").await,
525 &["rhode-island", "colorado", "oregon"],
526 );
527
528 async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec<String> {
529 db.fuzzy_search_users(query, 10)
530 .await
531 .unwrap()
532 .into_iter()
533 .map(|user| user.github_login)
534 .collect::<Vec<_>>()
535 }
536}
537
538#[gpui::test]
539async fn test_invite_codes() {
540 let test_db = TestDb::postgres(build_background_executor());
541 let db = test_db.db();
542
543 let NewUserResult { user_id: user1, .. } = db
544 .create_user(
545 "user1@example.com",
546 false,
547 NewUserParams {
548 github_login: "user1".into(),
549 github_user_id: 0,
550 invite_count: 0,
551 },
552 )
553 .await
554 .unwrap();
555
556 // Initially, user 1 has no invite code
557 assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
558
559 // Setting invite count to 0 when no code is assigned does not assign a new code
560 db.set_invite_count_for_user(user1, 0).await.unwrap();
561 assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
562
563 // User 1 creates an invite code that can be used twice.
564 db.set_invite_count_for_user(user1, 2).await.unwrap();
565 let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
566 assert_eq!(invite_count, 2);
567
568 // User 2 redeems the invite code and becomes a contact of user 1.
569 let user2_invite = db
570 .create_invite_from_code(
571 &invite_code,
572 "user2@example.com",
573 Some("user-2-device-id"),
574 true,
575 )
576 .await
577 .unwrap();
578 let NewUserResult {
579 user_id: user2,
580 inviting_user_id,
581 signup_device_id,
582 metrics_id,
583 } = db
584 .create_user_from_invite(
585 &user2_invite,
586 NewUserParams {
587 github_login: "user2".into(),
588 github_user_id: 2,
589 invite_count: 7,
590 },
591 )
592 .await
593 .unwrap()
594 .unwrap();
595 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
596 assert_eq!(invite_count, 1);
597 assert_eq!(inviting_user_id, Some(user1));
598 assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
599 assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
600 assert_eq!(
601 db.get_contacts(user1).await.unwrap(),
602 [Contact::Accepted {
603 user_id: user2,
604 should_notify: true,
605 busy: false,
606 }]
607 );
608 assert_eq!(
609 db.get_contacts(user2).await.unwrap(),
610 [Contact::Accepted {
611 user_id: user1,
612 should_notify: false,
613 busy: false,
614 }]
615 );
616 assert!(db.has_contact(user1, user2).await.unwrap());
617 assert!(db.has_contact(user2, user1).await.unwrap());
618 assert_eq!(
619 db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
620 7
621 );
622
623 // User 3 redeems the invite code and becomes a contact of user 1.
624 let user3_invite = db
625 .create_invite_from_code(&invite_code, "user3@example.com", None, true)
626 .await
627 .unwrap();
628 let NewUserResult {
629 user_id: user3,
630 inviting_user_id,
631 signup_device_id,
632 ..
633 } = db
634 .create_user_from_invite(
635 &user3_invite,
636 NewUserParams {
637 github_login: "user-3".into(),
638 github_user_id: 3,
639 invite_count: 3,
640 },
641 )
642 .await
643 .unwrap()
644 .unwrap();
645 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
646 assert_eq!(invite_count, 0);
647 assert_eq!(inviting_user_id, Some(user1));
648 assert!(signup_device_id.is_none());
649 assert_eq!(
650 db.get_contacts(user1).await.unwrap(),
651 [
652 Contact::Accepted {
653 user_id: user2,
654 should_notify: true,
655 busy: false,
656 },
657 Contact::Accepted {
658 user_id: user3,
659 should_notify: true,
660 busy: false,
661 }
662 ]
663 );
664 assert_eq!(
665 db.get_contacts(user3).await.unwrap(),
666 [Contact::Accepted {
667 user_id: user1,
668 should_notify: false,
669 busy: false,
670 }]
671 );
672 assert!(db.has_contact(user1, user3).await.unwrap());
673 assert!(db.has_contact(user3, user1).await.unwrap());
674 assert_eq!(
675 db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
676 3
677 );
678
679 // Trying to reedem the code for the third time results in an error.
680 db.create_invite_from_code(
681 &invite_code,
682 "user4@example.com",
683 Some("user-4-device-id"),
684 true,
685 )
686 .await
687 .unwrap_err();
688
689 // Invite count can be updated after the code has been created.
690 db.set_invite_count_for_user(user1, 2).await.unwrap();
691 let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
692 assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
693 assert_eq!(invite_count, 2);
694
695 // User 4 can now redeem the invite code and becomes a contact of user 1.
696 let user4_invite = db
697 .create_invite_from_code(
698 &invite_code,
699 "user4@example.com",
700 Some("user-4-device-id"),
701 true,
702 )
703 .await
704 .unwrap();
705 let user4 = db
706 .create_user_from_invite(
707 &user4_invite,
708 NewUserParams {
709 github_login: "user-4".into(),
710 github_user_id: 4,
711 invite_count: 5,
712 },
713 )
714 .await
715 .unwrap()
716 .unwrap()
717 .user_id;
718
719 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
720 assert_eq!(invite_count, 1);
721 assert_eq!(
722 db.get_contacts(user1).await.unwrap(),
723 [
724 Contact::Accepted {
725 user_id: user2,
726 should_notify: true,
727 busy: false,
728 },
729 Contact::Accepted {
730 user_id: user3,
731 should_notify: true,
732 busy: false,
733 },
734 Contact::Accepted {
735 user_id: user4,
736 should_notify: true,
737 busy: false,
738 }
739 ]
740 );
741 assert_eq!(
742 db.get_contacts(user4).await.unwrap(),
743 [Contact::Accepted {
744 user_id: user1,
745 should_notify: false,
746 busy: false,
747 }]
748 );
749 assert!(db.has_contact(user1, user4).await.unwrap());
750 assert!(db.has_contact(user4, user1).await.unwrap());
751 assert_eq!(
752 db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
753 5
754 );
755
756 // An existing user cannot redeem invite codes.
757 db.create_invite_from_code(
758 &invite_code,
759 "user2@example.com",
760 Some("user-2-device-id"),
761 true,
762 )
763 .await
764 .unwrap_err();
765 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
766 assert_eq!(invite_count, 1);
767
768 // A newer user can invite an existing one via a different email address
769 // than the one they used to sign up.
770 let user5 = db
771 .create_user(
772 "user5@example.com",
773 false,
774 NewUserParams {
775 github_login: "user5".into(),
776 github_user_id: 5,
777 invite_count: 0,
778 },
779 )
780 .await
781 .unwrap()
782 .user_id;
783 db.set_invite_count_for_user(user5, 5).await.unwrap();
784 let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
785 let user5_invite_to_user1 = db
786 .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
787 .await
788 .unwrap();
789 let user1_2 = db
790 .create_user_from_invite(
791 &user5_invite_to_user1,
792 NewUserParams {
793 github_login: "user1".into(),
794 github_user_id: 1,
795 invite_count: 5,
796 },
797 )
798 .await
799 .unwrap()
800 .unwrap()
801 .user_id;
802 assert_eq!(user1_2, user1);
803 assert_eq!(
804 db.get_contacts(user1).await.unwrap(),
805 [
806 Contact::Accepted {
807 user_id: user2,
808 should_notify: true,
809 busy: false,
810 },
811 Contact::Accepted {
812 user_id: user3,
813 should_notify: true,
814 busy: false,
815 },
816 Contact::Accepted {
817 user_id: user4,
818 should_notify: true,
819 busy: false,
820 },
821 Contact::Accepted {
822 user_id: user5,
823 should_notify: false,
824 busy: false,
825 }
826 ]
827 );
828 assert_eq!(
829 db.get_contacts(user5).await.unwrap(),
830 [Contact::Accepted {
831 user_id: user1,
832 should_notify: true,
833 busy: false,
834 }]
835 );
836 assert!(db.has_contact(user1, user5).await.unwrap());
837 assert!(db.has_contact(user5, user1).await.unwrap());
838}
839
840#[gpui::test]
841async fn test_multiple_signup_overwrite() {
842 let test_db = TestDb::postgres(build_background_executor());
843 let db = test_db.db();
844
845 let email_address = "user_1@example.com".to_string();
846
847 let initial_signup_created_at_milliseconds = 0;
848
849 let initial_signup = NewSignup {
850 email_address: email_address.clone(),
851 platform_mac: false,
852 platform_linux: true,
853 platform_windows: false,
854 editor_features: vec!["speed".into()],
855 programming_languages: vec!["rust".into(), "c".into()],
856 device_id: Some(format!("device_id")),
857 added_to_mailing_list: false,
858 created_at: Some(
859 DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
860 ),
861 };
862
863 db.create_signup(&initial_signup).await.unwrap();
864
865 let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
866
867 assert_eq!(
868 initial_signup_from_db.clone(),
869 signup::Model {
870 email_address: initial_signup.email_address,
871 platform_mac: initial_signup.platform_mac,
872 platform_linux: initial_signup.platform_linux,
873 platform_windows: initial_signup.platform_windows,
874 editor_features: Some(initial_signup.editor_features),
875 programming_languages: Some(initial_signup.programming_languages),
876 added_to_mailing_list: initial_signup.added_to_mailing_list,
877 ..initial_signup_from_db
878 }
879 );
880
881 let subsequent_signup = NewSignup {
882 email_address: email_address.clone(),
883 platform_mac: true,
884 platform_linux: false,
885 platform_windows: true,
886 editor_features: vec!["git integration".into(), "clean design".into()],
887 programming_languages: vec!["d".into(), "elm".into()],
888 device_id: Some(format!("different_device_id")),
889 added_to_mailing_list: true,
890 // subsequent signup happens next day
891 created_at: Some(
892 DateTime::from_timestamp_millis(
893 initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
894 )
895 .unwrap(),
896 ),
897 };
898
899 db.create_signup(&subsequent_signup).await.unwrap();
900
901 let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
902
903 assert_eq!(
904 subsequent_signup_from_db.clone(),
905 signup::Model {
906 platform_mac: subsequent_signup.platform_mac,
907 platform_linux: subsequent_signup.platform_linux,
908 platform_windows: subsequent_signup.platform_windows,
909 editor_features: Some(subsequent_signup.editor_features),
910 programming_languages: Some(subsequent_signup.programming_languages),
911 device_id: subsequent_signup.device_id,
912 added_to_mailing_list: subsequent_signup.added_to_mailing_list,
913 // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
914 created_at: initial_signup_from_db.created_at,
915 ..subsequent_signup_from_db
916 }
917 );
918}
919
920#[gpui::test]
921async fn test_signups() {
922 let test_db = TestDb::postgres(build_background_executor());
923 let db = test_db.db();
924
925 let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
926
927 let all_signups = usernames
928 .iter()
929 .enumerate()
930 .map(|(i, username)| NewSignup {
931 email_address: format!("{username}@example.com"),
932 platform_mac: true,
933 platform_linux: i % 2 == 0,
934 platform_windows: i % 4 == 0,
935 editor_features: vec!["speed".into()],
936 programming_languages: vec!["rust".into(), "c".into()],
937 device_id: Some(format!("device_id_{i}")),
938 added_to_mailing_list: i != 0, // One user failed to subscribe
939 created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
940 })
941 .collect::<Vec<NewSignup>>();
942
943 // people sign up on the waitlist
944 for signup in &all_signups {
945 // users can sign up multiple times without issues
946 for _ in 0..2 {
947 db.create_signup(&signup).await.unwrap();
948 }
949 }
950
951 assert_eq!(
952 db.get_waitlist_summary().await.unwrap(),
953 WaitlistSummary {
954 count: 8,
955 mac_count: 8,
956 linux_count: 4,
957 windows_count: 2,
958 unknown_count: 0,
959 }
960 );
961
962 // retrieve the next batch of signup emails to send
963 let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
964 let addresses = signups_batch1
965 .iter()
966 .map(|s| &s.email_address)
967 .collect::<Vec<_>>();
968 assert_eq!(
969 addresses,
970 &[
971 all_signups[0].email_address.as_str(),
972 all_signups[1].email_address.as_str(),
973 all_signups[2].email_address.as_str()
974 ]
975 );
976 assert_ne!(
977 signups_batch1[0].email_confirmation_code,
978 signups_batch1[1].email_confirmation_code
979 );
980
981 // the waitlist isn't updated until we record that the emails
982 // were successfully sent.
983 let signups_batch = db.get_unsent_invites(3).await.unwrap();
984 assert_eq!(signups_batch, signups_batch1);
985
986 // once the emails go out, we can retrieve the next batch
987 // of signups.
988 db.record_sent_invites(&signups_batch1).await.unwrap();
989 let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
990 let addresses = signups_batch2
991 .iter()
992 .map(|s| &s.email_address)
993 .collect::<Vec<_>>();
994 assert_eq!(
995 addresses,
996 &[
997 all_signups[3].email_address.as_str(),
998 all_signups[4].email_address.as_str(),
999 all_signups[5].email_address.as_str()
1000 ]
1001 );
1002
1003 // the sent invites are excluded from the summary.
1004 assert_eq!(
1005 db.get_waitlist_summary().await.unwrap(),
1006 WaitlistSummary {
1007 count: 5,
1008 mac_count: 5,
1009 linux_count: 2,
1010 windows_count: 1,
1011 unknown_count: 0,
1012 }
1013 );
1014
1015 // user completes the signup process by providing their
1016 // github account.
1017 let NewUserResult {
1018 user_id,
1019 inviting_user_id,
1020 signup_device_id,
1021 ..
1022 } = db
1023 .create_user_from_invite(
1024 &Invite {
1025 ..signups_batch1[0].clone()
1026 },
1027 NewUserParams {
1028 github_login: usernames[0].clone(),
1029 github_user_id: 0,
1030 invite_count: 5,
1031 },
1032 )
1033 .await
1034 .unwrap()
1035 .unwrap();
1036 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1037 assert!(inviting_user_id.is_none());
1038 assert_eq!(user.github_login, usernames[0]);
1039 assert_eq!(
1040 user.email_address,
1041 Some(all_signups[0].email_address.clone())
1042 );
1043 assert_eq!(user.invite_count, 5);
1044 assert_eq!(signup_device_id.unwrap(), "device_id_0");
1045
1046 // cannot redeem the same signup again.
1047 assert!(db
1048 .create_user_from_invite(
1049 &Invite {
1050 email_address: signups_batch1[0].email_address.clone(),
1051 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1052 },
1053 NewUserParams {
1054 github_login: "some-other-github_account".into(),
1055 github_user_id: 1,
1056 invite_count: 5,
1057 },
1058 )
1059 .await
1060 .unwrap()
1061 .is_none());
1062
1063 // cannot redeem a signup with the wrong confirmation code.
1064 db.create_user_from_invite(
1065 &Invite {
1066 email_address: signups_batch1[1].email_address.clone(),
1067 email_confirmation_code: "the-wrong-code".to_string(),
1068 },
1069 NewUserParams {
1070 github_login: usernames[1].clone(),
1071 github_user_id: 2,
1072 invite_count: 5,
1073 },
1074 )
1075 .await
1076 .unwrap_err();
1077}
1078
1079fn build_background_executor() -> Arc<Background> {
1080 Deterministic::new(0).build_background()
1081}