1use super::db::*;
2use gpui::executor::{Background, Deterministic};
3use std::sync::Arc;
4
5macro_rules! test_both_dbs {
6 ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {
7 #[gpui::test]
8 async fn $postgres_test_name() {
9 let test_db = PostgresTestDb::new(Deterministic::new(0).build_background());
10 let $db = test_db.db();
11 $body
12 }
13
14 #[gpui::test]
15 async fn $sqlite_test_name() {
16 let test_db = SqliteTestDb::new(Deterministic::new(0).build_background());
17 let $db = test_db.db();
18 $body
19 }
20 };
21}
22
23test_both_dbs!(
24 test_get_users_by_ids_postgres,
25 test_get_users_by_ids_sqlite,
26 db,
27 {
28 let mut user_ids = Vec::new();
29 for i in 1..=4 {
30 user_ids.push(
31 db.create_user(
32 &format!("user{i}@example.com"),
33 false,
34 NewUserParams {
35 github_login: format!("user{i}"),
36 github_user_id: i,
37 invite_count: 0,
38 },
39 )
40 .await
41 .unwrap()
42 .user_id,
43 );
44 }
45
46 assert_eq!(
47 db.get_users_by_ids(user_ids.clone()).await.unwrap(),
48 vec![
49 User {
50 id: user_ids[0],
51 github_login: "user1".to_string(),
52 github_user_id: Some(1),
53 email_address: Some("user1@example.com".to_string()),
54 admin: false,
55 ..Default::default()
56 },
57 User {
58 id: user_ids[1],
59 github_login: "user2".to_string(),
60 github_user_id: Some(2),
61 email_address: Some("user2@example.com".to_string()),
62 admin: false,
63 ..Default::default()
64 },
65 User {
66 id: user_ids[2],
67 github_login: "user3".to_string(),
68 github_user_id: Some(3),
69 email_address: Some("user3@example.com".to_string()),
70 admin: false,
71 ..Default::default()
72 },
73 User {
74 id: user_ids[3],
75 github_login: "user4".to_string(),
76 github_user_id: Some(4),
77 email_address: Some("user4@example.com".to_string()),
78 admin: false,
79 ..Default::default()
80 }
81 ]
82 );
83 }
84);
85
86test_both_dbs!(
87 test_get_user_by_github_account_postgres,
88 test_get_user_by_github_account_sqlite,
89 db,
90 {
91 let user_id1 = db
92 .create_user(
93 "user1@example.com",
94 false,
95 NewUserParams {
96 github_login: "login1".into(),
97 github_user_id: 101,
98 invite_count: 0,
99 },
100 )
101 .await
102 .unwrap()
103 .user_id;
104 let user_id2 = db
105 .create_user(
106 "user2@example.com",
107 false,
108 NewUserParams {
109 github_login: "login2".into(),
110 github_user_id: 102,
111 invite_count: 0,
112 },
113 )
114 .await
115 .unwrap()
116 .user_id;
117
118 let user = db
119 .get_user_by_github_account("login1", None)
120 .await
121 .unwrap()
122 .unwrap();
123 assert_eq!(user.id, user_id1);
124 assert_eq!(&user.github_login, "login1");
125 assert_eq!(user.github_user_id, Some(101));
126
127 assert!(db
128 .get_user_by_github_account("non-existent-login", None)
129 .await
130 .unwrap()
131 .is_none());
132
133 let user = db
134 .get_user_by_github_account("the-new-login2", Some(102))
135 .await
136 .unwrap()
137 .unwrap();
138 assert_eq!(user.id, user_id2);
139 assert_eq!(&user.github_login, "the-new-login2");
140 assert_eq!(user.github_user_id, Some(102));
141 }
142);
143
144test_both_dbs!(
145 test_create_access_tokens_postgres,
146 test_create_access_tokens_sqlite,
147 db,
148 {
149 let user = db
150 .create_user(
151 "u1@example.com",
152 false,
153 NewUserParams {
154 github_login: "u1".into(),
155 github_user_id: 1,
156 invite_count: 0,
157 },
158 )
159 .await
160 .unwrap()
161 .user_id;
162
163 db.create_access_token_hash(user, "h1", 3).await.unwrap();
164 db.create_access_token_hash(user, "h2", 3).await.unwrap();
165 assert_eq!(
166 db.get_access_token_hashes(user).await.unwrap(),
167 &["h2".to_string(), "h1".to_string()]
168 );
169
170 db.create_access_token_hash(user, "h3", 3).await.unwrap();
171 assert_eq!(
172 db.get_access_token_hashes(user).await.unwrap(),
173 &["h3".to_string(), "h2".to_string(), "h1".to_string(),]
174 );
175
176 db.create_access_token_hash(user, "h4", 3).await.unwrap();
177 assert_eq!(
178 db.get_access_token_hashes(user).await.unwrap(),
179 &["h4".to_string(), "h3".to_string(), "h2".to_string(),]
180 );
181
182 db.create_access_token_hash(user, "h5", 3).await.unwrap();
183 assert_eq!(
184 db.get_access_token_hashes(user).await.unwrap(),
185 &["h5".to_string(), "h4".to_string(), "h3".to_string()]
186 );
187 }
188);
189
190test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
191 let mut user_ids = Vec::new();
192 for i in 0..3 {
193 user_ids.push(
194 db.create_user(
195 &format!("user{i}@example.com"),
196 false,
197 NewUserParams {
198 github_login: format!("user{i}"),
199 github_user_id: i,
200 invite_count: 0,
201 },
202 )
203 .await
204 .unwrap()
205 .user_id,
206 );
207 }
208
209 let user_1 = user_ids[0];
210 let user_2 = user_ids[1];
211 let user_3 = user_ids[2];
212
213 // User starts with no contacts
214 assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
215
216 // User requests a contact. Both users see the pending request.
217 db.send_contact_request(user_1, user_2).await.unwrap();
218 assert!(!db.has_contact(user_1, user_2).await.unwrap());
219 assert!(!db.has_contact(user_2, user_1).await.unwrap());
220 assert_eq!(
221 db.get_contacts(user_1).await.unwrap(),
222 &[Contact::Outgoing { user_id: user_2 }],
223 );
224 assert_eq!(
225 db.get_contacts(user_2).await.unwrap(),
226 &[Contact::Incoming {
227 user_id: user_1,
228 should_notify: true
229 }]
230 );
231
232 // User 2 dismisses the contact request notification without accepting or rejecting.
233 // We shouldn't notify them again.
234 db.dismiss_contact_notification(user_1, user_2)
235 .await
236 .unwrap_err();
237 db.dismiss_contact_notification(user_2, user_1)
238 .await
239 .unwrap();
240 assert_eq!(
241 db.get_contacts(user_2).await.unwrap(),
242 &[Contact::Incoming {
243 user_id: user_1,
244 should_notify: false
245 }]
246 );
247
248 // User can't accept their own contact request
249 db.respond_to_contact_request(user_1, user_2, true)
250 .await
251 .unwrap_err();
252
253 // User accepts a contact request. Both users see the contact.
254 db.respond_to_contact_request(user_2, user_1, true)
255 .await
256 .unwrap();
257 assert_eq!(
258 db.get_contacts(user_1).await.unwrap(),
259 &[Contact::Accepted {
260 user_id: user_2,
261 should_notify: true,
262 busy: false,
263 }],
264 );
265 assert!(db.has_contact(user_1, user_2).await.unwrap());
266 assert!(db.has_contact(user_2, user_1).await.unwrap());
267 assert_eq!(
268 db.get_contacts(user_2).await.unwrap(),
269 &[Contact::Accepted {
270 user_id: user_1,
271 should_notify: false,
272 busy: false,
273 }]
274 );
275
276 // Users cannot re-request existing contacts.
277 db.send_contact_request(user_1, user_2).await.unwrap_err();
278 db.send_contact_request(user_2, user_1).await.unwrap_err();
279
280 // Users can't dismiss notifications of them accepting other users' requests.
281 db.dismiss_contact_notification(user_2, user_1)
282 .await
283 .unwrap_err();
284 assert_eq!(
285 db.get_contacts(user_1).await.unwrap(),
286 &[Contact::Accepted {
287 user_id: user_2,
288 should_notify: true,
289 busy: false,
290 }]
291 );
292
293 // Users can dismiss notifications of other users accepting their requests.
294 db.dismiss_contact_notification(user_1, user_2)
295 .await
296 .unwrap();
297 assert_eq!(
298 db.get_contacts(user_1).await.unwrap(),
299 &[Contact::Accepted {
300 user_id: user_2,
301 should_notify: false,
302 busy: false,
303 }]
304 );
305
306 // Users send each other concurrent contact requests and
307 // see that they are immediately accepted.
308 db.send_contact_request(user_1, user_3).await.unwrap();
309 db.send_contact_request(user_3, user_1).await.unwrap();
310 assert_eq!(
311 db.get_contacts(user_1).await.unwrap(),
312 &[
313 Contact::Accepted {
314 user_id: user_2,
315 should_notify: false,
316 busy: false,
317 },
318 Contact::Accepted {
319 user_id: user_3,
320 should_notify: false,
321 busy: false,
322 }
323 ]
324 );
325 assert_eq!(
326 db.get_contacts(user_3).await.unwrap(),
327 &[Contact::Accepted {
328 user_id: user_1,
329 should_notify: false,
330 busy: false,
331 }],
332 );
333
334 // User declines a contact request. Both users see that it is gone.
335 db.send_contact_request(user_2, user_3).await.unwrap();
336 db.respond_to_contact_request(user_3, user_2, false)
337 .await
338 .unwrap();
339 assert!(!db.has_contact(user_2, user_3).await.unwrap());
340 assert!(!db.has_contact(user_3, user_2).await.unwrap());
341 assert_eq!(
342 db.get_contacts(user_2).await.unwrap(),
343 &[Contact::Accepted {
344 user_id: user_1,
345 should_notify: false,
346 busy: false,
347 }]
348 );
349 assert_eq!(
350 db.get_contacts(user_3).await.unwrap(),
351 &[Contact::Accepted {
352 user_id: user_1,
353 should_notify: false,
354 busy: false,
355 }],
356 );
357});
358
359test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
360 let NewUserResult {
361 user_id: user1,
362 metrics_id: metrics_id1,
363 ..
364 } = db
365 .create_user(
366 "person1@example.com",
367 false,
368 NewUserParams {
369 github_login: "person1".into(),
370 github_user_id: 101,
371 invite_count: 5,
372 },
373 )
374 .await
375 .unwrap();
376 let NewUserResult {
377 user_id: user2,
378 metrics_id: metrics_id2,
379 ..
380 } = db
381 .create_user(
382 "person2@example.com",
383 false,
384 NewUserParams {
385 github_login: "person2".into(),
386 github_user_id: 102,
387 invite_count: 5,
388 },
389 )
390 .await
391 .unwrap();
392
393 assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
394 assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
395 assert_eq!(metrics_id1.len(), 36);
396 assert_eq!(metrics_id2.len(), 36);
397 assert_ne!(metrics_id1, metrics_id2);
398});
399
400#[test]
401fn test_fuzzy_like_string() {
402 assert_eq!(DefaultDb::fuzzy_like_string("abcd"), "%a%b%c%d%");
403 assert_eq!(DefaultDb::fuzzy_like_string("x y"), "%x%y%");
404 assert_eq!(DefaultDb::fuzzy_like_string(" z "), "%z%");
405}
406
407#[gpui::test]
408async fn test_fuzzy_search_users() {
409 let test_db = PostgresTestDb::new(build_background_executor());
410 let db = test_db.db();
411 for (i, github_login) in [
412 "California",
413 "colorado",
414 "oregon",
415 "washington",
416 "florida",
417 "delaware",
418 "rhode-island",
419 ]
420 .into_iter()
421 .enumerate()
422 {
423 db.create_user(
424 &format!("{github_login}@example.com"),
425 false,
426 NewUserParams {
427 github_login: github_login.into(),
428 github_user_id: i as i32,
429 invite_count: 0,
430 },
431 )
432 .await
433 .unwrap();
434 }
435
436 assert_eq!(
437 fuzzy_search_user_names(db, "clr").await,
438 &["colorado", "California"]
439 );
440 assert_eq!(
441 fuzzy_search_user_names(db, "ro").await,
442 &["rhode-island", "colorado", "oregon"],
443 );
444
445 async fn fuzzy_search_user_names(db: &Db<sqlx::Postgres>, query: &str) -> Vec<String> {
446 db.fuzzy_search_users(query, 10)
447 .await
448 .unwrap()
449 .into_iter()
450 .map(|user| user.github_login)
451 .collect::<Vec<_>>()
452 }
453}
454
455#[gpui::test]
456async fn test_invite_codes() {
457 let test_db = PostgresTestDb::new(build_background_executor());
458 let db = test_db.db();
459
460 let NewUserResult { user_id: user1, .. } = db
461 .create_user(
462 "user1@example.com",
463 false,
464 NewUserParams {
465 github_login: "user1".into(),
466 github_user_id: 0,
467 invite_count: 0,
468 },
469 )
470 .await
471 .unwrap();
472
473 // Initially, user 1 has no invite code
474 assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
475
476 // Setting invite count to 0 when no code is assigned does not assign a new code
477 db.set_invite_count_for_user(user1, 0).await.unwrap();
478 assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
479
480 // User 1 creates an invite code that can be used twice.
481 db.set_invite_count_for_user(user1, 2).await.unwrap();
482 let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
483 assert_eq!(invite_count, 2);
484
485 // User 2 redeems the invite code and becomes a contact of user 1.
486 let user2_invite = db
487 .create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
488 .await
489 .unwrap();
490 let NewUserResult {
491 user_id: user2,
492 inviting_user_id,
493 signup_device_id,
494 metrics_id,
495 } = db
496 .create_user_from_invite(
497 &user2_invite,
498 NewUserParams {
499 github_login: "user2".into(),
500 github_user_id: 2,
501 invite_count: 7,
502 },
503 )
504 .await
505 .unwrap()
506 .unwrap();
507 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
508 assert_eq!(invite_count, 1);
509 assert_eq!(inviting_user_id, Some(user1));
510 assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
511 assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
512 assert_eq!(
513 db.get_contacts(user1).await.unwrap(),
514 [Contact::Accepted {
515 user_id: user2,
516 should_notify: true,
517 busy: false,
518 }]
519 );
520 assert_eq!(
521 db.get_contacts(user2).await.unwrap(),
522 [Contact::Accepted {
523 user_id: user1,
524 should_notify: false,
525 busy: false,
526 }]
527 );
528 assert_eq!(
529 db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
530 7
531 );
532
533 // User 3 redeems the invite code and becomes a contact of user 1.
534 let user3_invite = db
535 .create_invite_from_code(&invite_code, "user3@example.com", None)
536 .await
537 .unwrap();
538 let NewUserResult {
539 user_id: user3,
540 inviting_user_id,
541 signup_device_id,
542 ..
543 } = db
544 .create_user_from_invite(
545 &user3_invite,
546 NewUserParams {
547 github_login: "user-3".into(),
548 github_user_id: 3,
549 invite_count: 3,
550 },
551 )
552 .await
553 .unwrap()
554 .unwrap();
555 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
556 assert_eq!(invite_count, 0);
557 assert_eq!(inviting_user_id, Some(user1));
558 assert!(signup_device_id.is_none());
559 assert_eq!(
560 db.get_contacts(user1).await.unwrap(),
561 [
562 Contact::Accepted {
563 user_id: user2,
564 should_notify: true,
565 busy: false,
566 },
567 Contact::Accepted {
568 user_id: user3,
569 should_notify: true,
570 busy: false,
571 }
572 ]
573 );
574 assert_eq!(
575 db.get_contacts(user3).await.unwrap(),
576 [Contact::Accepted {
577 user_id: user1,
578 should_notify: false,
579 busy: false,
580 }]
581 );
582 assert_eq!(
583 db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
584 3
585 );
586
587 // Trying to reedem the code for the third time results in an error.
588 db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
589 .await
590 .unwrap_err();
591
592 // Invite count can be updated after the code has been created.
593 db.set_invite_count_for_user(user1, 2).await.unwrap();
594 let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
595 assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
596 assert_eq!(invite_count, 2);
597
598 // User 4 can now redeem the invite code and becomes a contact of user 1.
599 let user4_invite = db
600 .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
601 .await
602 .unwrap();
603 let user4 = db
604 .create_user_from_invite(
605 &user4_invite,
606 NewUserParams {
607 github_login: "user-4".into(),
608 github_user_id: 4,
609 invite_count: 5,
610 },
611 )
612 .await
613 .unwrap()
614 .unwrap()
615 .user_id;
616
617 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
618 assert_eq!(invite_count, 1);
619 assert_eq!(
620 db.get_contacts(user1).await.unwrap(),
621 [
622 Contact::Accepted {
623 user_id: user2,
624 should_notify: true,
625 busy: false,
626 },
627 Contact::Accepted {
628 user_id: user3,
629 should_notify: true,
630 busy: false,
631 },
632 Contact::Accepted {
633 user_id: user4,
634 should_notify: true,
635 busy: false,
636 }
637 ]
638 );
639 assert_eq!(
640 db.get_contacts(user4).await.unwrap(),
641 [Contact::Accepted {
642 user_id: user1,
643 should_notify: false,
644 busy: false,
645 }]
646 );
647 assert_eq!(
648 db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
649 5
650 );
651
652 // An existing user cannot redeem invite codes.
653 db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
654 .await
655 .unwrap_err();
656 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
657 assert_eq!(invite_count, 1);
658}
659
660#[gpui::test]
661async fn test_signups() {
662 let test_db = PostgresTestDb::new(build_background_executor());
663 let db = test_db.db();
664
665 // people sign up on the waitlist
666 for i in 0..8 {
667 db.create_signup(Signup {
668 email_address: format!("person-{i}@example.com"),
669 platform_mac: true,
670 platform_linux: i % 2 == 0,
671 platform_windows: i % 4 == 0,
672 editor_features: vec!["speed".into()],
673 programming_languages: vec!["rust".into(), "c".into()],
674 device_id: Some(format!("device_id_{i}")),
675 })
676 .await
677 .unwrap();
678 }
679
680 assert_eq!(
681 db.get_waitlist_summary().await.unwrap(),
682 WaitlistSummary {
683 count: 8,
684 mac_count: 8,
685 linux_count: 4,
686 windows_count: 2,
687 unknown_count: 0,
688 }
689 );
690
691 // retrieve the next batch of signup emails to send
692 let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
693 let addresses = signups_batch1
694 .iter()
695 .map(|s| &s.email_address)
696 .collect::<Vec<_>>();
697 assert_eq!(
698 addresses,
699 &[
700 "person-0@example.com",
701 "person-1@example.com",
702 "person-2@example.com"
703 ]
704 );
705 assert_ne!(
706 signups_batch1[0].email_confirmation_code,
707 signups_batch1[1].email_confirmation_code
708 );
709
710 // the waitlist isn't updated until we record that the emails
711 // were successfully sent.
712 let signups_batch = db.get_unsent_invites(3).await.unwrap();
713 assert_eq!(signups_batch, signups_batch1);
714
715 // once the emails go out, we can retrieve the next batch
716 // of signups.
717 db.record_sent_invites(&signups_batch1).await.unwrap();
718 let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
719 let addresses = signups_batch2
720 .iter()
721 .map(|s| &s.email_address)
722 .collect::<Vec<_>>();
723 assert_eq!(
724 addresses,
725 &[
726 "person-3@example.com",
727 "person-4@example.com",
728 "person-5@example.com"
729 ]
730 );
731
732 // the sent invites are excluded from the summary.
733 assert_eq!(
734 db.get_waitlist_summary().await.unwrap(),
735 WaitlistSummary {
736 count: 5,
737 mac_count: 5,
738 linux_count: 2,
739 windows_count: 1,
740 unknown_count: 0,
741 }
742 );
743
744 // user completes the signup process by providing their
745 // github account.
746 let NewUserResult {
747 user_id,
748 inviting_user_id,
749 signup_device_id,
750 ..
751 } = db
752 .create_user_from_invite(
753 &Invite {
754 email_address: signups_batch1[0].email_address.clone(),
755 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
756 },
757 NewUserParams {
758 github_login: "person-0".into(),
759 github_user_id: 0,
760 invite_count: 5,
761 },
762 )
763 .await
764 .unwrap()
765 .unwrap();
766 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
767 assert!(inviting_user_id.is_none());
768 assert_eq!(user.github_login, "person-0");
769 assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
770 assert_eq!(user.invite_count, 5);
771 assert_eq!(signup_device_id.unwrap(), "device_id_0");
772
773 // cannot redeem the same signup again.
774 assert!(db
775 .create_user_from_invite(
776 &Invite {
777 email_address: signups_batch1[0].email_address.clone(),
778 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
779 },
780 NewUserParams {
781 github_login: "some-other-github_account".into(),
782 github_user_id: 1,
783 invite_count: 5,
784 },
785 )
786 .await
787 .unwrap()
788 .is_none());
789
790 // cannot redeem a signup with the wrong confirmation code.
791 db.create_user_from_invite(
792 &Invite {
793 email_address: signups_batch1[1].email_address.clone(),
794 email_confirmation_code: "the-wrong-code".to_string(),
795 },
796 NewUserParams {
797 github_login: "person-1".into(),
798 github_user_id: 2,
799 invite_count: 5,
800 },
801 )
802 .await
803 .unwrap_err();
804}
805
806fn build_background_executor() -> Arc<Background> {
807 Deterministic::new(0).build_background()
808}