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 i32,
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    let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
671
672    let all_signups = usernames
673        .iter()
674        .enumerate()
675        .map(|(i, username)| NewSignup {
676            email_address: format!("{username}@example.com"),
677            platform_mac: true,
678            platform_linux: i % 2 == 0,
679            platform_windows: i % 4 == 0,
680            editor_features: vec!["speed".into()],
681            programming_languages: vec!["rust".into(), "c".into()],
682            device_id: Some(format!("device_id_{i}")),
683            added_to_mailing_list: i != 0, // One user failed to subscribe
684        })
685        .collect::<Vec<NewSignup>>();
686
687    // people sign up on the waitlist
688    for signup in &all_signups {
689        // users can sign up multiple times without issues
690        for _ in 0..2 {
691            db.create_signup(&signup).await.unwrap();
692        }
693    }
694
695    assert_eq!(
696        db.get_waitlist_summary().await.unwrap(),
697        WaitlistSummary {
698            count: 8,
699            mac_count: 8,
700            linux_count: 4,
701            windows_count: 2,
702            unknown_count: 0,
703        }
704    );
705
706    // retrieve the next batch of signup emails to send
707    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
708    let addresses = signups_batch1
709        .iter()
710        .map(|s| &s.email_address)
711        .collect::<Vec<_>>();
712    assert_eq!(
713        addresses,
714        &[
715            all_signups[0].email_address.as_str(),
716            all_signups[1].email_address.as_str(),
717            all_signups[2].email_address.as_str()
718        ]
719    );
720    assert_ne!(
721        signups_batch1[0].email_confirmation_code,
722        signups_batch1[1].email_confirmation_code
723    );
724
725    // the waitlist isn't updated until we record that the emails
726    // were successfully sent.
727    let signups_batch = db.get_unsent_invites(3).await.unwrap();
728    assert_eq!(signups_batch, signups_batch1);
729
730    // once the emails go out, we can retrieve the next batch
731    // of signups.
732    db.record_sent_invites(&signups_batch1).await.unwrap();
733    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
734    let addresses = signups_batch2
735        .iter()
736        .map(|s| &s.email_address)
737        .collect::<Vec<_>>();
738    assert_eq!(
739        addresses,
740        &[
741            all_signups[3].email_address.as_str(),
742            all_signups[4].email_address.as_str(),
743            all_signups[5].email_address.as_str()
744        ]
745    );
746
747    // the sent invites are excluded from the summary.
748    assert_eq!(
749        db.get_waitlist_summary().await.unwrap(),
750        WaitlistSummary {
751            count: 5,
752            mac_count: 5,
753            linux_count: 2,
754            windows_count: 1,
755            unknown_count: 0,
756        }
757    );
758
759    // user completes the signup process by providing their
760    // github account.
761    let NewUserResult {
762        user_id,
763        inviting_user_id,
764        signup_device_id,
765        ..
766    } = db
767        .create_user_from_invite(
768            &Invite {
769                ..signups_batch1[0].clone()
770            },
771            NewUserParams {
772                github_login: usernames[0].clone(),
773                github_user_id: 0,
774                invite_count: 5,
775            },
776        )
777        .await
778        .unwrap()
779        .unwrap();
780    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
781    assert!(inviting_user_id.is_none());
782    assert_eq!(user.github_login, usernames[0]);
783    assert_eq!(
784        user.email_address,
785        Some(all_signups[0].email_address.clone())
786    );
787    assert_eq!(user.invite_count, 5);
788    assert_eq!(signup_device_id.unwrap(), "device_id_0");
789
790    // cannot redeem the same signup again.
791    assert!(db
792        .create_user_from_invite(
793            &Invite {
794                email_address: signups_batch1[0].email_address.clone(),
795                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
796            },
797            NewUserParams {
798                github_login: "some-other-github_account".into(),
799                github_user_id: 1,
800                invite_count: 5,
801            },
802        )
803        .await
804        .unwrap()
805        .is_none());
806
807    // cannot redeem a signup with the wrong confirmation code.
808    db.create_user_from_invite(
809        &Invite {
810            email_address: signups_batch1[1].email_address.clone(),
811            email_confirmation_code: "the-wrong-code".to_string(),
812        },
813        NewUserParams {
814            github_login: usernames[1].clone(),
815            github_user_id: 2,
816            invite_count: 5,
817        },
818    )
819    .await
820    .unwrap_err();
821}
822
823fn build_background_executor() -> Arc<Background> {
824    Deterministic::new(0).build_background()
825}