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 }],
263 );
264 assert!(db.has_contact(user_1, user_2).await.unwrap());
265 assert!(db.has_contact(user_2, user_1).await.unwrap());
266 assert_eq!(
267 db.get_contacts(user_2).await.unwrap(),
268 &[Contact::Accepted {
269 user_id: user_1,
270 should_notify: false,
271 }]
272 );
273
274 // Users cannot re-request existing contacts.
275 db.send_contact_request(user_1, user_2).await.unwrap_err();
276 db.send_contact_request(user_2, user_1).await.unwrap_err();
277
278 // Users can't dismiss notifications of them accepting other users' requests.
279 db.dismiss_contact_notification(user_2, user_1)
280 .await
281 .unwrap_err();
282 assert_eq!(
283 db.get_contacts(user_1).await.unwrap(),
284 &[Contact::Accepted {
285 user_id: user_2,
286 should_notify: true,
287 }]
288 );
289
290 // Users can dismiss notifications of other users accepting their requests.
291 db.dismiss_contact_notification(user_1, user_2)
292 .await
293 .unwrap();
294 assert_eq!(
295 db.get_contacts(user_1).await.unwrap(),
296 &[Contact::Accepted {
297 user_id: user_2,
298 should_notify: false,
299 }]
300 );
301
302 // Users send each other concurrent contact requests and
303 // see that they are immediately accepted.
304 db.send_contact_request(user_1, user_3).await.unwrap();
305 db.send_contact_request(user_3, user_1).await.unwrap();
306 assert_eq!(
307 db.get_contacts(user_1).await.unwrap(),
308 &[
309 Contact::Accepted {
310 user_id: user_2,
311 should_notify: false,
312 },
313 Contact::Accepted {
314 user_id: user_3,
315 should_notify: false
316 }
317 ]
318 );
319 assert_eq!(
320 db.get_contacts(user_3).await.unwrap(),
321 &[Contact::Accepted {
322 user_id: user_1,
323 should_notify: false
324 }],
325 );
326
327 // User declines a contact request. Both users see that it is gone.
328 db.send_contact_request(user_2, user_3).await.unwrap();
329 db.respond_to_contact_request(user_3, user_2, false)
330 .await
331 .unwrap();
332 assert!(!db.has_contact(user_2, user_3).await.unwrap());
333 assert!(!db.has_contact(user_3, user_2).await.unwrap());
334 assert_eq!(
335 db.get_contacts(user_2).await.unwrap(),
336 &[Contact::Accepted {
337 user_id: user_1,
338 should_notify: false
339 }]
340 );
341 assert_eq!(
342 db.get_contacts(user_3).await.unwrap(),
343 &[Contact::Accepted {
344 user_id: user_1,
345 should_notify: false
346 }],
347 );
348});
349
350test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
351 let NewUserResult {
352 user_id: user1,
353 metrics_id: metrics_id1,
354 ..
355 } = db
356 .create_user(
357 "person1@example.com",
358 false,
359 NewUserParams {
360 github_login: "person1".into(),
361 github_user_id: 101,
362 invite_count: 5,
363 },
364 )
365 .await
366 .unwrap();
367 let NewUserResult {
368 user_id: user2,
369 metrics_id: metrics_id2,
370 ..
371 } = db
372 .create_user(
373 "person2@example.com",
374 false,
375 NewUserParams {
376 github_login: "person2".into(),
377 github_user_id: 102,
378 invite_count: 5,
379 },
380 )
381 .await
382 .unwrap();
383
384 assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
385 assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
386 assert_eq!(metrics_id1.len(), 36);
387 assert_eq!(metrics_id2.len(), 36);
388 assert_ne!(metrics_id1, metrics_id2);
389});
390
391#[test]
392fn test_fuzzy_like_string() {
393 assert_eq!(DefaultDb::fuzzy_like_string("abcd"), "%a%b%c%d%");
394 assert_eq!(DefaultDb::fuzzy_like_string("x y"), "%x%y%");
395 assert_eq!(DefaultDb::fuzzy_like_string(" z "), "%z%");
396}
397
398#[gpui::test]
399async fn test_fuzzy_search_users() {
400 let test_db = PostgresTestDb::new(build_background_executor());
401 let db = test_db.db();
402 for (i, github_login) in [
403 "California",
404 "colorado",
405 "oregon",
406 "washington",
407 "florida",
408 "delaware",
409 "rhode-island",
410 ]
411 .into_iter()
412 .enumerate()
413 {
414 db.create_user(
415 &format!("{github_login}@example.com"),
416 false,
417 NewUserParams {
418 github_login: github_login.into(),
419 github_user_id: i as i32,
420 invite_count: 0,
421 },
422 )
423 .await
424 .unwrap();
425 }
426
427 assert_eq!(
428 fuzzy_search_user_names(db, "clr").await,
429 &["colorado", "California"]
430 );
431 assert_eq!(
432 fuzzy_search_user_names(db, "ro").await,
433 &["rhode-island", "colorado", "oregon"],
434 );
435
436 async fn fuzzy_search_user_names(db: &Db<sqlx::Postgres>, query: &str) -> Vec<String> {
437 db.fuzzy_search_users(query, 10)
438 .await
439 .unwrap()
440 .into_iter()
441 .map(|user| user.github_login)
442 .collect::<Vec<_>>()
443 }
444}
445
446#[gpui::test]
447async fn test_invite_codes() {
448 let test_db = PostgresTestDb::new(build_background_executor());
449 let db = test_db.db();
450
451 let NewUserResult { user_id: user1, .. } = db
452 .create_user(
453 "user1@example.com",
454 false,
455 NewUserParams {
456 github_login: "user1".into(),
457 github_user_id: 0,
458 invite_count: 0,
459 },
460 )
461 .await
462 .unwrap();
463
464 // Initially, user 1 has no invite code
465 assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
466
467 // Setting invite count to 0 when no code is assigned does not assign a new code
468 db.set_invite_count_for_user(user1, 0).await.unwrap();
469 assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
470
471 // User 1 creates an invite code that can be used twice.
472 db.set_invite_count_for_user(user1, 2).await.unwrap();
473 let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
474 assert_eq!(invite_count, 2);
475
476 // User 2 redeems the invite code and becomes a contact of user 1.
477 let user2_invite = db
478 .create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
479 .await
480 .unwrap();
481 let NewUserResult {
482 user_id: user2,
483 inviting_user_id,
484 signup_device_id,
485 metrics_id,
486 } = db
487 .create_user_from_invite(
488 &user2_invite,
489 NewUserParams {
490 github_login: "user2".into(),
491 github_user_id: 2,
492 invite_count: 7,
493 },
494 )
495 .await
496 .unwrap()
497 .unwrap();
498 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
499 assert_eq!(invite_count, 1);
500 assert_eq!(inviting_user_id, Some(user1));
501 assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
502 assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
503 assert_eq!(
504 db.get_contacts(user1).await.unwrap(),
505 [Contact::Accepted {
506 user_id: user2,
507 should_notify: true
508 }]
509 );
510 assert_eq!(
511 db.get_contacts(user2).await.unwrap(),
512 [Contact::Accepted {
513 user_id: user1,
514 should_notify: false
515 }]
516 );
517 assert_eq!(
518 db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
519 7
520 );
521
522 // User 3 redeems the invite code and becomes a contact of user 1.
523 let user3_invite = db
524 .create_invite_from_code(&invite_code, "user3@example.com", None)
525 .await
526 .unwrap();
527 let NewUserResult {
528 user_id: user3,
529 inviting_user_id,
530 signup_device_id,
531 ..
532 } = db
533 .create_user_from_invite(
534 &user3_invite,
535 NewUserParams {
536 github_login: "user-3".into(),
537 github_user_id: 3,
538 invite_count: 3,
539 },
540 )
541 .await
542 .unwrap()
543 .unwrap();
544 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
545 assert_eq!(invite_count, 0);
546 assert_eq!(inviting_user_id, Some(user1));
547 assert!(signup_device_id.is_none());
548 assert_eq!(
549 db.get_contacts(user1).await.unwrap(),
550 [
551 Contact::Accepted {
552 user_id: user2,
553 should_notify: true
554 },
555 Contact::Accepted {
556 user_id: user3,
557 should_notify: true
558 }
559 ]
560 );
561 assert_eq!(
562 db.get_contacts(user3).await.unwrap(),
563 [Contact::Accepted {
564 user_id: user1,
565 should_notify: false
566 }]
567 );
568 assert_eq!(
569 db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
570 3
571 );
572
573 // Trying to reedem the code for the third time results in an error.
574 db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
575 .await
576 .unwrap_err();
577
578 // Invite count can be updated after the code has been created.
579 db.set_invite_count_for_user(user1, 2).await.unwrap();
580 let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
581 assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
582 assert_eq!(invite_count, 2);
583
584 // User 4 can now redeem the invite code and becomes a contact of user 1.
585 let user4_invite = db
586 .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
587 .await
588 .unwrap();
589 let user4 = db
590 .create_user_from_invite(
591 &user4_invite,
592 NewUserParams {
593 github_login: "user-4".into(),
594 github_user_id: 4,
595 invite_count: 5,
596 },
597 )
598 .await
599 .unwrap()
600 .unwrap()
601 .user_id;
602
603 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
604 assert_eq!(invite_count, 1);
605 assert_eq!(
606 db.get_contacts(user1).await.unwrap(),
607 [
608 Contact::Accepted {
609 user_id: user2,
610 should_notify: true
611 },
612 Contact::Accepted {
613 user_id: user3,
614 should_notify: true
615 },
616 Contact::Accepted {
617 user_id: user4,
618 should_notify: true
619 }
620 ]
621 );
622 assert_eq!(
623 db.get_contacts(user4).await.unwrap(),
624 [Contact::Accepted {
625 user_id: user1,
626 should_notify: false
627 }]
628 );
629 assert_eq!(
630 db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
631 5
632 );
633
634 // An existing user cannot redeem invite codes.
635 db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
636 .await
637 .unwrap_err();
638 let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
639 assert_eq!(invite_count, 1);
640}
641
642#[gpui::test]
643async fn test_signups() {
644 let test_db = PostgresTestDb::new(build_background_executor());
645 let db = test_db.db();
646
647 let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
648
649 let all_signups = usernames
650 .iter()
651 .enumerate()
652 .map(|(i, username)| Signup {
653 email_address: format!("{username}@example.com"),
654 platform_mac: true,
655 platform_linux: i % 2 == 0,
656 platform_windows: i % 4 == 0,
657 editor_features: vec!["speed".into()],
658 programming_languages: vec!["rust".into(), "c".into()],
659 device_id: Some(format!("device_id_{i}")),
660 added_to_mailing_list: i != 0, // One user failed to subscribe
661 })
662 .collect::<Vec<Signup>>();
663
664 // people sign up on the waitlist
665 for signup in &all_signups {
666 // users can sign up multiple times without issues
667 for _ in 0..2 {
668 db.create_signup(&signup).await.unwrap();
669 }
670 }
671
672 assert_eq!(
673 db.get_waitlist_summary().await.unwrap(),
674 WaitlistSummary {
675 count: 8,
676 mac_count: 8,
677 linux_count: 4,
678 windows_count: 2,
679 unknown_count: 0,
680 }
681 );
682
683 // retrieve the next batch of signup emails to send
684 let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
685 let addresses = signups_batch1
686 .iter()
687 .map(|s| &s.email_address)
688 .collect::<Vec<_>>();
689 assert_eq!(
690 addresses,
691 &[
692 all_signups[0].email_address.as_str(),
693 all_signups[1].email_address.as_str(),
694 all_signups[2].email_address.as_str()
695 ]
696 );
697 assert_ne!(
698 signups_batch1[0].email_confirmation_code,
699 signups_batch1[1].email_confirmation_code
700 );
701
702 // the waitlist isn't updated until we record that the emails
703 // were successfully sent.
704 let signups_batch = db.get_unsent_invites(3).await.unwrap();
705 assert_eq!(signups_batch, signups_batch1);
706
707 // once the emails go out, we can retrieve the next batch
708 // of signups.
709 db.record_sent_invites(&signups_batch1).await.unwrap();
710 let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
711 let addresses = signups_batch2
712 .iter()
713 .map(|s| &s.email_address)
714 .collect::<Vec<_>>();
715 assert_eq!(
716 addresses,
717 &[
718 all_signups[3].email_address.as_str(),
719 all_signups[4].email_address.as_str(),
720 all_signups[5].email_address.as_str()
721 ]
722 );
723
724 // the sent invites are excluded from the summary.
725 assert_eq!(
726 db.get_waitlist_summary().await.unwrap(),
727 WaitlistSummary {
728 count: 5,
729 mac_count: 5,
730 linux_count: 2,
731 windows_count: 1,
732 unknown_count: 0,
733 }
734 );
735
736 // user completes the signup process by providing their
737 // github account.
738 let NewUserResult {
739 user_id,
740 inviting_user_id,
741 signup_device_id,
742 ..
743 } = db
744 .create_user_from_invite(
745 &Invite {
746 ..signups_batch1[0].clone()
747 },
748 NewUserParams {
749 github_login: usernames[0].clone(),
750 github_user_id: 0,
751 invite_count: 5,
752 },
753 )
754 .await
755 .unwrap()
756 .unwrap();
757 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
758 assert!(inviting_user_id.is_none());
759 assert_eq!(user.github_login, usernames[0]);
760 assert_eq!(
761 user.email_address,
762 Some(all_signups[0].email_address.clone())
763 );
764 assert_eq!(user.invite_count, 5);
765 assert_eq!(signup_device_id.unwrap(), "device_id_0");
766
767 // cannot redeem the same signup again.
768 assert!(db
769 .create_user_from_invite(
770 &Invite {
771 email_address: signups_batch1[0].email_address.clone(),
772 email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
773 },
774 NewUserParams {
775 github_login: "some-other-github_account".into(),
776 github_user_id: 1,
777 invite_count: 5,
778 },
779 )
780 .await
781 .unwrap()
782 .is_none());
783
784 // cannot redeem a signup with the wrong confirmation code.
785 db.create_user_from_invite(
786 &Invite {
787 email_address: signups_batch1[1].email_address.clone(),
788 email_confirmation_code: "the-wrong-code".to_string(),
789 },
790 NewUserParams {
791 github_login: usernames[1].clone(),
792 github_user_id: 2,
793 invite_count: 5,
794 },
795 )
796 .await
797 .unwrap_err();
798}
799
800fn build_background_executor() -> Arc<Background> {
801 Deterministic::new(0).build_background()
802}