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(&invite_code, "user2@example.com", Some("user-2-device-id"))
571 .await
572 .unwrap();
573 let NewUserResult {
574 user_id: user2,
575 inviting_user_id,
576 signup_device_id,
577 metrics_id,
578 } = db
579 .create_user_from_invite(
580 &user2_invite,
581 NewUserParams {
582 github_login: "user2".into(),
583 github_user_id: 2,
584 invite_count: 7,
585 },
586 )
587 .await
588 .unwrap()
589 .unwrap();
590 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
591 assert_eq!(invite_count, 1);
592 assert_eq!(inviting_user_id, Some(user1));
593 assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
594 assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
595 assert_eq!(
596 db.get_contacts(user1).await.unwrap(),
597 [Contact::Accepted {
598 user_id: user2,
599 should_notify: true,
600 busy: false,
601 }]
602 );
603 assert_eq!(
604 db.get_contacts(user2).await.unwrap(),
605 [Contact::Accepted {
606 user_id: user1,
607 should_notify: false,
608 busy: false,
609 }]
610 );
611 assert!(db.has_contact(user1, user2).await.unwrap());
612 assert!(db.has_contact(user2, user1).await.unwrap());
613 assert_eq!(
614 db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
615 7
616 );
617
618 // User 3 redeems the invite code and becomes a contact of user 1.
619 let user3_invite = db
620 .create_invite_from_code(&invite_code, "user3@example.com", None)
621 .await
622 .unwrap();
623 let NewUserResult {
624 user_id: user3,
625 inviting_user_id,
626 signup_device_id,
627 ..
628 } = db
629 .create_user_from_invite(
630 &user3_invite,
631 NewUserParams {
632 github_login: "user-3".into(),
633 github_user_id: 3,
634 invite_count: 3,
635 },
636 )
637 .await
638 .unwrap()
639 .unwrap();
640 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
641 assert_eq!(invite_count, 0);
642 assert_eq!(inviting_user_id, Some(user1));
643 assert!(signup_device_id.is_none());
644 assert_eq!(
645 db.get_contacts(user1).await.unwrap(),
646 [
647 Contact::Accepted {
648 user_id: user2,
649 should_notify: true,
650 busy: false,
651 },
652 Contact::Accepted {
653 user_id: user3,
654 should_notify: true,
655 busy: false,
656 }
657 ]
658 );
659 assert_eq!(
660 db.get_contacts(user3).await.unwrap(),
661 [Contact::Accepted {
662 user_id: user1,
663 should_notify: false,
664 busy: false,
665 }]
666 );
667 assert!(db.has_contact(user1, user3).await.unwrap());
668 assert!(db.has_contact(user3, user1).await.unwrap());
669 assert_eq!(
670 db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
671 3
672 );
673
674 // Trying to reedem the code for the third time results in an error.
675 db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
676 .await
677 .unwrap_err();
678
679 // Invite count can be updated after the code has been created.
680 db.set_invite_count_for_user(user1, 2).await.unwrap();
681 let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
682 assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
683 assert_eq!(invite_count, 2);
684
685 // User 4 can now redeem the invite code and becomes a contact of user 1.
686 let user4_invite = db
687 .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
688 .await
689 .unwrap();
690 let user4 = db
691 .create_user_from_invite(
692 &user4_invite,
693 NewUserParams {
694 github_login: "user-4".into(),
695 github_user_id: 4,
696 invite_count: 5,
697 },
698 )
699 .await
700 .unwrap()
701 .unwrap()
702 .user_id;
703
704 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
705 assert_eq!(invite_count, 1);
706 assert_eq!(
707 db.get_contacts(user1).await.unwrap(),
708 [
709 Contact::Accepted {
710 user_id: user2,
711 should_notify: true,
712 busy: false,
713 },
714 Contact::Accepted {
715 user_id: user3,
716 should_notify: true,
717 busy: false,
718 },
719 Contact::Accepted {
720 user_id: user4,
721 should_notify: true,
722 busy: false,
723 }
724 ]
725 );
726 assert_eq!(
727 db.get_contacts(user4).await.unwrap(),
728 [Contact::Accepted {
729 user_id: user1,
730 should_notify: false,
731 busy: false,
732 }]
733 );
734 assert!(db.has_contact(user1, user4).await.unwrap());
735 assert!(db.has_contact(user4, user1).await.unwrap());
736 assert_eq!(
737 db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
738 5
739 );
740
741 // An existing user cannot redeem invite codes.
742 db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
743 .await
744 .unwrap_err();
745 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
746 assert_eq!(invite_count, 1);
747
748 // A newer user can invite an existing one via a different email address
749 // than the one they used to sign up.
750 let user5 = db
751 .create_user(
752 "user5@example.com",
753 false,
754 NewUserParams {
755 github_login: "user5".into(),
756 github_user_id: 5,
757 invite_count: 0,
758 },
759 )
760 .await
761 .unwrap()
762 .user_id;
763 db.set_invite_count_for_user(user5, 5).await.unwrap();
764 let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
765 let user5_invite_to_user1 = db
766 .create_invite_from_code(&user5_invite_code, "user1@different.com", None)
767 .await
768 .unwrap();
769 let user1_2 = db
770 .create_user_from_invite(
771 &user5_invite_to_user1,
772 NewUserParams {
773 github_login: "user1".into(),
774 github_user_id: 1,
775 invite_count: 5,
776 },
777 )
778 .await
779 .unwrap()
780 .unwrap()
781 .user_id;
782 assert_eq!(user1_2, user1);
783 assert_eq!(
784 db.get_contacts(user1).await.unwrap(),
785 [
786 Contact::Accepted {
787 user_id: user2,
788 should_notify: true,
789 busy: false,
790 },
791 Contact::Accepted {
792 user_id: user3,
793 should_notify: true,
794 busy: false,
795 },
796 Contact::Accepted {
797 user_id: user4,
798 should_notify: true,
799 busy: false,
800 },
801 Contact::Accepted {
802 user_id: user5,
803 should_notify: false,
804 busy: false,
805 }
806 ]
807 );
808 assert_eq!(
809 db.get_contacts(user5).await.unwrap(),
810 [Contact::Accepted {
811 user_id: user1,
812 should_notify: true,
813 busy: false,
814 }]
815 );
816 assert!(db.has_contact(user1, user5).await.unwrap());
817 assert!(db.has_contact(user5, user1).await.unwrap());
818}
819
820#[gpui::test]
821async fn test_multiple_signup_overwrite() {
822 let test_db = TestDb::postgres(build_background_executor());
823 let db = test_db.db();
824
825 let email_address = "user_1@example.com".to_string();
826
827 let initial_signup_created_at_milliseconds = 0;
828
829 let initial_signup = NewSignup {
830 email_address: email_address.clone(),
831 platform_mac: false,
832 platform_linux: true,
833 platform_windows: false,
834 editor_features: vec!["speed".into()],
835 programming_languages: vec!["rust".into(), "c".into()],
836 device_id: Some(format!("device_id")),
837 added_to_mailing_list: false,
838 created_at: Some(
839 DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
840 ),
841 };
842
843 db.create_signup(&initial_signup).await.unwrap();
844
845 let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
846
847 assert_eq!(
848 initial_signup_from_db.clone(),
849 signup::Model {
850 email_address: initial_signup.email_address,
851 platform_mac: initial_signup.platform_mac,
852 platform_linux: initial_signup.platform_linux,
853 platform_windows: initial_signup.platform_windows,
854 editor_features: Some(initial_signup.editor_features),
855 programming_languages: Some(initial_signup.programming_languages),
856 added_to_mailing_list: initial_signup.added_to_mailing_list,
857 ..initial_signup_from_db
858 }
859 );
860
861 let subsequent_signup = NewSignup {
862 email_address: email_address.clone(),
863 platform_mac: true,
864 platform_linux: false,
865 platform_windows: true,
866 editor_features: vec!["git integration".into(), "clean design".into()],
867 programming_languages: vec!["d".into(), "elm".into()],
868 device_id: Some(format!("different_device_id")),
869 added_to_mailing_list: true,
870 // subsequent signup happens next day
871 created_at: Some(
872 DateTime::from_timestamp_millis(
873 initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
874 )
875 .unwrap(),
876 ),
877 };
878
879 db.create_signup(&subsequent_signup).await.unwrap();
880
881 let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
882
883 assert_eq!(
884 subsequent_signup_from_db.clone(),
885 signup::Model {
886 platform_mac: subsequent_signup.platform_mac,
887 platform_linux: subsequent_signup.platform_linux,
888 platform_windows: subsequent_signup.platform_windows,
889 editor_features: Some(subsequent_signup.editor_features),
890 programming_languages: Some(subsequent_signup.programming_languages),
891 device_id: subsequent_signup.device_id,
892 added_to_mailing_list: subsequent_signup.added_to_mailing_list,
893 // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
894 created_at: initial_signup_from_db.created_at,
895 ..subsequent_signup_from_db
896 }
897 );
898}
899
900#[gpui::test]
901async fn test_signups() {
902 let test_db = TestDb::postgres(build_background_executor());
903 let db = test_db.db();
904
905 let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
906
907 let all_signups = usernames
908 .iter()
909 .enumerate()
910 .map(|(i, username)| NewSignup {
911 email_address: format!("{username}@example.com"),
912 platform_mac: true,
913 platform_linux: i % 2 == 0,
914 platform_windows: i % 4 == 0,
915 editor_features: vec!["speed".into()],
916 programming_languages: vec!["rust".into(), "c".into()],
917 device_id: Some(format!("device_id_{i}")),
918 added_to_mailing_list: i != 0, // One user failed to subscribe
919 created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
920 })
921 .collect::<Vec<NewSignup>>();
922
923 // people sign up on the waitlist
924 for signup in &all_signups {
925 // users can sign up multiple times without issues
926 for _ in 0..2 {
927 db.create_signup(&signup).await.unwrap();
928 }
929 }
930
931 assert_eq!(
932 db.get_waitlist_summary().await.unwrap(),
933 WaitlistSummary {
934 count: 8,
935 mac_count: 8,
936 linux_count: 4,
937 windows_count: 2,
938 unknown_count: 0,
939 }
940 );
941
942 // retrieve the next batch of signup emails to send
943 let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
944 let addresses = signups_batch1
945 .iter()
946 .map(|s| &s.email_address)
947 .collect::<Vec<_>>();
948 assert_eq!(
949 addresses,
950 &[
951 all_signups[0].email_address.as_str(),
952 all_signups[1].email_address.as_str(),
953 all_signups[2].email_address.as_str()
954 ]
955 );
956 assert_ne!(
957 signups_batch1[0].email_confirmation_code,
958 signups_batch1[1].email_confirmation_code
959 );
960
961 // the waitlist isn't updated until we record that the emails
962 // were successfully sent.
963 let signups_batch = db.get_unsent_invites(3).await.unwrap();
964 assert_eq!(signups_batch, signups_batch1);
965
966 // once the emails go out, we can retrieve the next batch
967 // of signups.
968 db.record_sent_invites(&signups_batch1).await.unwrap();
969 let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
970 let addresses = signups_batch2
971 .iter()
972 .map(|s| &s.email_address)
973 .collect::<Vec<_>>();
974 assert_eq!(
975 addresses,
976 &[
977 all_signups[3].email_address.as_str(),
978 all_signups[4].email_address.as_str(),
979 all_signups[5].email_address.as_str()
980 ]
981 );
982
983 // the sent invites are excluded from the summary.
984 assert_eq!(
985 db.get_waitlist_summary().await.unwrap(),
986 WaitlistSummary {
987 count: 5,
988 mac_count: 5,
989 linux_count: 2,
990 windows_count: 1,
991 unknown_count: 0,
992 }
993 );
994
995 // user completes the signup process by providing their
996 // github account.
997 let NewUserResult {
998 user_id,
999 inviting_user_id,
1000 signup_device_id,
1001 ..
1002 } = db
1003 .create_user_from_invite(
1004 &Invite {
1005 ..signups_batch1[0].clone()
1006 },
1007 NewUserParams {
1008 github_login: usernames[0].clone(),
1009 github_user_id: 0,
1010 invite_count: 5,
1011 },
1012 )
1013 .await
1014 .unwrap()
1015 .unwrap();
1016 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1017 assert!(inviting_user_id.is_none());
1018 assert_eq!(user.github_login, usernames[0]);
1019 assert_eq!(
1020 user.email_address,
1021 Some(all_signups[0].email_address.clone())
1022 );
1023 assert_eq!(user.invite_count, 5);
1024 assert_eq!(signup_device_id.unwrap(), "device_id_0");
1025
1026 // cannot redeem the same signup again.
1027 assert!(db
1028 .create_user_from_invite(
1029 &Invite {
1030 email_address: signups_batch1[0].email_address.clone(),
1031 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1032 },
1033 NewUserParams {
1034 github_login: "some-other-github_account".into(),
1035 github_user_id: 1,
1036 invite_count: 5,
1037 },
1038 )
1039 .await
1040 .unwrap()
1041 .is_none());
1042
1043 // cannot redeem a signup with the wrong confirmation code.
1044 db.create_user_from_invite(
1045 &Invite {
1046 email_address: signups_batch1[1].email_address.clone(),
1047 email_confirmation_code: "the-wrong-code".to_string(),
1048 },
1049 NewUserParams {
1050 github_login: usernames[1].clone(),
1051 github_user_id: 2,
1052 invite_count: 5,
1053 },
1054 )
1055 .await
1056 .unwrap_err();
1057}
1058
1059fn build_background_executor() -> Arc<Background> {
1060 Deterministic::new(0).build_background()
1061}