db_tests.rs

  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}