tests.rs

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