tests.rs

   1use super::*;
   2use gpui::executor::{Background, Deterministic};
   3use std::sync::Arc;
   4
   5#[cfg(test)]
   6use pretty_assertions::{assert_eq, assert_ne};
   7
   8macro_rules! test_both_dbs {
   9    ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {
  10        #[gpui::test]
  11        async fn $postgres_test_name() {
  12            let test_db = TestDb::postgres(Deterministic::new(0).build_background());
  13            let $db = test_db.db();
  14            $body
  15        }
  16
  17        #[gpui::test]
  18        async fn $sqlite_test_name() {
  19            let test_db = TestDb::sqlite(Deterministic::new(0).build_background());
  20            let $db = test_db.db();
  21            $body
  22        }
  23    };
  24}
  25
  26test_both_dbs!(
  27    test_get_users_by_ids_postgres,
  28    test_get_users_by_ids_sqlite,
  29    db,
  30    {
  31        let mut user_ids = Vec::new();
  32        let mut user_metric_ids = Vec::new();
  33        for i in 1..=4 {
  34            let user = db
  35                .create_user(
  36                    &format!("user{i}@example.com"),
  37                    false,
  38                    NewUserParams {
  39                        github_login: format!("user{i}"),
  40                        github_user_id: i,
  41                        invite_count: 0,
  42                    },
  43                )
  44                .await
  45                .unwrap();
  46            user_ids.push(user.user_id);
  47            user_metric_ids.push(user.metrics_id);
  48        }
  49
  50        assert_eq!(
  51            db.get_users_by_ids(user_ids.clone()).await.unwrap(),
  52            vec![
  53                User {
  54                    id: user_ids[0],
  55                    github_login: "user1".to_string(),
  56                    github_user_id: Some(1),
  57                    email_address: Some("user1@example.com".to_string()),
  58                    admin: false,
  59                    metrics_id: user_metric_ids[0].parse().unwrap(),
  60                    ..Default::default()
  61                },
  62                User {
  63                    id: user_ids[1],
  64                    github_login: "user2".to_string(),
  65                    github_user_id: Some(2),
  66                    email_address: Some("user2@example.com".to_string()),
  67                    admin: false,
  68                    metrics_id: user_metric_ids[1].parse().unwrap(),
  69                    ..Default::default()
  70                },
  71                User {
  72                    id: user_ids[2],
  73                    github_login: "user3".to_string(),
  74                    github_user_id: Some(3),
  75                    email_address: Some("user3@example.com".to_string()),
  76                    admin: false,
  77                    metrics_id: user_metric_ids[2].parse().unwrap(),
  78                    ..Default::default()
  79                },
  80                User {
  81                    id: user_ids[3],
  82                    github_login: "user4".to_string(),
  83                    github_user_id: Some(4),
  84                    email_address: Some("user4@example.com".to_string()),
  85                    admin: false,
  86                    metrics_id: user_metric_ids[3].parse().unwrap(),
  87                    ..Default::default()
  88                }
  89            ]
  90        );
  91    }
  92);
  93
  94test_both_dbs!(
  95    test_get_or_create_user_by_github_account_postgres,
  96    test_get_or_create_user_by_github_account_sqlite,
  97    db,
  98    {
  99        let user_id1 = db
 100            .create_user(
 101                "user1@example.com",
 102                false,
 103                NewUserParams {
 104                    github_login: "login1".into(),
 105                    github_user_id: 101,
 106                    invite_count: 0,
 107                },
 108            )
 109            .await
 110            .unwrap()
 111            .user_id;
 112        let user_id2 = db
 113            .create_user(
 114                "user2@example.com",
 115                false,
 116                NewUserParams {
 117                    github_login: "login2".into(),
 118                    github_user_id: 102,
 119                    invite_count: 0,
 120                },
 121            )
 122            .await
 123            .unwrap()
 124            .user_id;
 125
 126        let user = db
 127            .get_or_create_user_by_github_account("login1", None, None)
 128            .await
 129            .unwrap()
 130            .unwrap();
 131        assert_eq!(user.id, user_id1);
 132        assert_eq!(&user.github_login, "login1");
 133        assert_eq!(user.github_user_id, Some(101));
 134
 135        assert!(db
 136            .get_or_create_user_by_github_account("non-existent-login", None, None)
 137            .await
 138            .unwrap()
 139            .is_none());
 140
 141        let user = db
 142            .get_or_create_user_by_github_account("the-new-login2", Some(102), None)
 143            .await
 144            .unwrap()
 145            .unwrap();
 146        assert_eq!(user.id, user_id2);
 147        assert_eq!(&user.github_login, "the-new-login2");
 148        assert_eq!(user.github_user_id, Some(102));
 149
 150        let user = db
 151            .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
 152            .await
 153            .unwrap()
 154            .unwrap();
 155        assert_eq!(&user.github_login, "login3");
 156        assert_eq!(user.github_user_id, Some(103));
 157        assert_eq!(user.email_address, Some("user3@example.com".into()));
 158    }
 159);
 160
 161test_both_dbs!(
 162    test_create_access_tokens_postgres,
 163    test_create_access_tokens_sqlite,
 164    db,
 165    {
 166        let user = db
 167            .create_user(
 168                "u1@example.com",
 169                false,
 170                NewUserParams {
 171                    github_login: "u1".into(),
 172                    github_user_id: 1,
 173                    invite_count: 0,
 174                },
 175            )
 176            .await
 177            .unwrap()
 178            .user_id;
 179
 180        db.create_access_token_hash(user, "h1", 3).await.unwrap();
 181        db.create_access_token_hash(user, "h2", 3).await.unwrap();
 182        assert_eq!(
 183            db.get_access_token_hashes(user).await.unwrap(),
 184            &["h2".to_string(), "h1".to_string()]
 185        );
 186
 187        db.create_access_token_hash(user, "h3", 3).await.unwrap();
 188        assert_eq!(
 189            db.get_access_token_hashes(user).await.unwrap(),
 190            &["h3".to_string(), "h2".to_string(), "h1".to_string(),]
 191        );
 192
 193        db.create_access_token_hash(user, "h4", 3).await.unwrap();
 194        assert_eq!(
 195            db.get_access_token_hashes(user).await.unwrap(),
 196            &["h4".to_string(), "h3".to_string(), "h2".to_string(),]
 197        );
 198
 199        db.create_access_token_hash(user, "h5", 3).await.unwrap();
 200        assert_eq!(
 201            db.get_access_token_hashes(user).await.unwrap(),
 202            &["h5".to_string(), "h4".to_string(), "h3".to_string()]
 203        );
 204    }
 205);
 206
 207test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
 208    let mut user_ids = Vec::new();
 209    for i in 0..3 {
 210        user_ids.push(
 211            db.create_user(
 212                &format!("user{i}@example.com"),
 213                false,
 214                NewUserParams {
 215                    github_login: format!("user{i}"),
 216                    github_user_id: i,
 217                    invite_count: 0,
 218                },
 219            )
 220            .await
 221            .unwrap()
 222            .user_id,
 223        );
 224    }
 225
 226    let user_1 = user_ids[0];
 227    let user_2 = user_ids[1];
 228    let user_3 = user_ids[2];
 229
 230    // User starts with no contacts
 231    assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
 232
 233    // User requests a contact. Both users see the pending request.
 234    db.send_contact_request(user_1, user_2).await.unwrap();
 235    assert!(!db.has_contact(user_1, user_2).await.unwrap());
 236    assert!(!db.has_contact(user_2, user_1).await.unwrap());
 237    assert_eq!(
 238        db.get_contacts(user_1).await.unwrap(),
 239        &[Contact::Outgoing { user_id: user_2 }],
 240    );
 241    assert_eq!(
 242        db.get_contacts(user_2).await.unwrap(),
 243        &[Contact::Incoming {
 244            user_id: user_1,
 245            should_notify: true
 246        }]
 247    );
 248
 249    // User 2 dismisses the contact request notification without accepting or rejecting.
 250    // We shouldn't notify them again.
 251    db.dismiss_contact_notification(user_1, user_2)
 252        .await
 253        .unwrap_err();
 254    db.dismiss_contact_notification(user_2, user_1)
 255        .await
 256        .unwrap();
 257    assert_eq!(
 258        db.get_contacts(user_2).await.unwrap(),
 259        &[Contact::Incoming {
 260            user_id: user_1,
 261            should_notify: false
 262        }]
 263    );
 264
 265    // User can't accept their own contact request
 266    db.respond_to_contact_request(user_1, user_2, true)
 267        .await
 268        .unwrap_err();
 269
 270    // User accepts a contact request. Both users see the contact.
 271    db.respond_to_contact_request(user_2, user_1, true)
 272        .await
 273        .unwrap();
 274    assert_eq!(
 275        db.get_contacts(user_1).await.unwrap(),
 276        &[Contact::Accepted {
 277            user_id: user_2,
 278            should_notify: true,
 279            busy: false,
 280        }],
 281    );
 282    assert!(db.has_contact(user_1, user_2).await.unwrap());
 283    assert!(db.has_contact(user_2, user_1).await.unwrap());
 284    assert_eq!(
 285        db.get_contacts(user_2).await.unwrap(),
 286        &[Contact::Accepted {
 287            user_id: user_1,
 288            should_notify: false,
 289            busy: false,
 290        }]
 291    );
 292
 293    // Users cannot re-request existing contacts.
 294    db.send_contact_request(user_1, user_2).await.unwrap_err();
 295    db.send_contact_request(user_2, user_1).await.unwrap_err();
 296
 297    // Users can't dismiss notifications of them accepting other users' requests.
 298    db.dismiss_contact_notification(user_2, user_1)
 299        .await
 300        .unwrap_err();
 301    assert_eq!(
 302        db.get_contacts(user_1).await.unwrap(),
 303        &[Contact::Accepted {
 304            user_id: user_2,
 305            should_notify: true,
 306            busy: false,
 307        }]
 308    );
 309
 310    // Users can dismiss notifications of other users accepting their requests.
 311    db.dismiss_contact_notification(user_1, user_2)
 312        .await
 313        .unwrap();
 314    assert_eq!(
 315        db.get_contacts(user_1).await.unwrap(),
 316        &[Contact::Accepted {
 317            user_id: user_2,
 318            should_notify: false,
 319            busy: false,
 320        }]
 321    );
 322
 323    // Users send each other concurrent contact requests and
 324    // see that they are immediately accepted.
 325    db.send_contact_request(user_1, user_3).await.unwrap();
 326    db.send_contact_request(user_3, user_1).await.unwrap();
 327    assert_eq!(
 328        db.get_contacts(user_1).await.unwrap(),
 329        &[
 330            Contact::Accepted {
 331                user_id: user_2,
 332                should_notify: false,
 333                busy: false,
 334            },
 335            Contact::Accepted {
 336                user_id: user_3,
 337                should_notify: false,
 338                busy: false,
 339            }
 340        ]
 341    );
 342    assert_eq!(
 343        db.get_contacts(user_3).await.unwrap(),
 344        &[Contact::Accepted {
 345            user_id: user_1,
 346            should_notify: false,
 347            busy: false,
 348        }],
 349    );
 350
 351    // User declines a contact request. Both users see that it is gone.
 352    db.send_contact_request(user_2, user_3).await.unwrap();
 353    db.respond_to_contact_request(user_3, user_2, false)
 354        .await
 355        .unwrap();
 356    assert!(!db.has_contact(user_2, user_3).await.unwrap());
 357    assert!(!db.has_contact(user_3, user_2).await.unwrap());
 358    assert_eq!(
 359        db.get_contacts(user_2).await.unwrap(),
 360        &[Contact::Accepted {
 361            user_id: user_1,
 362            should_notify: false,
 363            busy: false,
 364        }]
 365    );
 366    assert_eq!(
 367        db.get_contacts(user_3).await.unwrap(),
 368        &[Contact::Accepted {
 369            user_id: user_1,
 370            should_notify: false,
 371            busy: false,
 372        }],
 373    );
 374});
 375
 376test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
 377    let NewUserResult {
 378        user_id: user1,
 379        metrics_id: metrics_id1,
 380        ..
 381    } = db
 382        .create_user(
 383            "person1@example.com",
 384            false,
 385            NewUserParams {
 386                github_login: "person1".into(),
 387                github_user_id: 101,
 388                invite_count: 5,
 389            },
 390        )
 391        .await
 392        .unwrap();
 393    let NewUserResult {
 394        user_id: user2,
 395        metrics_id: metrics_id2,
 396        ..
 397    } = db
 398        .create_user(
 399            "person2@example.com",
 400            false,
 401            NewUserParams {
 402                github_login: "person2".into(),
 403                github_user_id: 102,
 404                invite_count: 5,
 405            },
 406        )
 407        .await
 408        .unwrap();
 409
 410    assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
 411    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
 412    assert_eq!(metrics_id1.len(), 36);
 413    assert_eq!(metrics_id2.len(), 36);
 414    assert_ne!(metrics_id1, metrics_id2);
 415});
 416
 417test_both_dbs!(
 418    test_project_count_postgres,
 419    test_project_count_sqlite,
 420    db,
 421    {
 422        let owner_id = db.create_server("test").await.unwrap().0 as u32;
 423
 424        let user1 = db
 425            .create_user(
 426                &format!("admin@example.com"),
 427                true,
 428                NewUserParams {
 429                    github_login: "admin".into(),
 430                    github_user_id: 0,
 431                    invite_count: 0,
 432                },
 433            )
 434            .await
 435            .unwrap();
 436        let user2 = db
 437            .create_user(
 438                &format!("user@example.com"),
 439                false,
 440                NewUserParams {
 441                    github_login: "user".into(),
 442                    github_user_id: 1,
 443                    invite_count: 0,
 444                },
 445            )
 446            .await
 447            .unwrap();
 448
 449        let room_id = RoomId::from_proto(
 450            db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
 451                .await
 452                .unwrap()
 453                .id,
 454        );
 455        db.call(
 456            room_id,
 457            user1.user_id,
 458            ConnectionId { owner_id, id: 0 },
 459            user2.user_id,
 460            None,
 461        )
 462        .await
 463        .unwrap();
 464        db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
 465            .await
 466            .unwrap();
 467        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
 468
 469        db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
 470            .await
 471            .unwrap();
 472        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
 473
 474        db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
 475            .await
 476            .unwrap();
 477        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
 478
 479        // Projects shared by admins aren't counted.
 480        db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
 481            .await
 482            .unwrap();
 483        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
 484
 485        db.leave_room(ConnectionId { owner_id, id: 1 })
 486            .await
 487            .unwrap();
 488        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
 489    }
 490);
 491
 492#[test]
 493fn test_fuzzy_like_string() {
 494    assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%");
 495    assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%");
 496    assert_eq!(Database::fuzzy_like_string(" z  "), "%z%");
 497}
 498
 499#[gpui::test]
 500async fn test_fuzzy_search_users() {
 501    let test_db = TestDb::postgres(build_background_executor());
 502    let db = test_db.db();
 503    for (i, github_login) in [
 504        "California",
 505        "colorado",
 506        "oregon",
 507        "washington",
 508        "florida",
 509        "delaware",
 510        "rhode-island",
 511    ]
 512    .into_iter()
 513    .enumerate()
 514    {
 515        db.create_user(
 516            &format!("{github_login}@example.com"),
 517            false,
 518            NewUserParams {
 519                github_login: github_login.into(),
 520                github_user_id: i as i32,
 521                invite_count: 0,
 522            },
 523        )
 524        .await
 525        .unwrap();
 526    }
 527
 528    assert_eq!(
 529        fuzzy_search_user_names(db, "clr").await,
 530        &["colorado", "California"]
 531    );
 532    assert_eq!(
 533        fuzzy_search_user_names(db, "ro").await,
 534        &["rhode-island", "colorado", "oregon"],
 535    );
 536
 537    async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec<String> {
 538        db.fuzzy_search_users(query, 10)
 539            .await
 540            .unwrap()
 541            .into_iter()
 542            .map(|user| user.github_login)
 543            .collect::<Vec<_>>()
 544    }
 545}
 546
 547#[gpui::test]
 548async fn test_invite_codes() {
 549    let test_db = TestDb::postgres(build_background_executor());
 550    let db = test_db.db();
 551
 552    let NewUserResult { user_id: user1, .. } = db
 553        .create_user(
 554            "user1@example.com",
 555            false,
 556            NewUserParams {
 557                github_login: "user1".into(),
 558                github_user_id: 0,
 559                invite_count: 0,
 560            },
 561        )
 562        .await
 563        .unwrap();
 564
 565    // Initially, user 1 has no invite code
 566    assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
 567
 568    // Setting invite count to 0 when no code is assigned does not assign a new code
 569    db.set_invite_count_for_user(user1, 0).await.unwrap();
 570    assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
 571
 572    // User 1 creates an invite code that can be used twice.
 573    db.set_invite_count_for_user(user1, 2).await.unwrap();
 574    let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 575    assert_eq!(invite_count, 2);
 576
 577    // User 2 redeems the invite code and becomes a contact of user 1.
 578    let user2_invite = db
 579        .create_invite_from_code(
 580            &invite_code,
 581            "user2@example.com",
 582            Some("user-2-device-id"),
 583            true,
 584        )
 585        .await
 586        .unwrap();
 587    let NewUserResult {
 588        user_id: user2,
 589        inviting_user_id,
 590        signup_device_id,
 591        metrics_id,
 592    } = db
 593        .create_user_from_invite(
 594            &user2_invite,
 595            NewUserParams {
 596                github_login: "user2".into(),
 597                github_user_id: 2,
 598                invite_count: 7,
 599            },
 600        )
 601        .await
 602        .unwrap()
 603        .unwrap();
 604    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 605    assert_eq!(invite_count, 1);
 606    assert_eq!(inviting_user_id, Some(user1));
 607    assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
 608    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
 609    assert_eq!(
 610        db.get_contacts(user1).await.unwrap(),
 611        [Contact::Accepted {
 612            user_id: user2,
 613            should_notify: true,
 614            busy: false,
 615        }]
 616    );
 617    assert_eq!(
 618        db.get_contacts(user2).await.unwrap(),
 619        [Contact::Accepted {
 620            user_id: user1,
 621            should_notify: false,
 622            busy: false,
 623        }]
 624    );
 625    assert!(db.has_contact(user1, user2).await.unwrap());
 626    assert!(db.has_contact(user2, user1).await.unwrap());
 627    assert_eq!(
 628        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
 629        7
 630    );
 631
 632    // User 3 redeems the invite code and becomes a contact of user 1.
 633    let user3_invite = db
 634        .create_invite_from_code(&invite_code, "user3@example.com", None, true)
 635        .await
 636        .unwrap();
 637    let NewUserResult {
 638        user_id: user3,
 639        inviting_user_id,
 640        signup_device_id,
 641        ..
 642    } = db
 643        .create_user_from_invite(
 644            &user3_invite,
 645            NewUserParams {
 646                github_login: "user-3".into(),
 647                github_user_id: 3,
 648                invite_count: 3,
 649            },
 650        )
 651        .await
 652        .unwrap()
 653        .unwrap();
 654    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 655    assert_eq!(invite_count, 0);
 656    assert_eq!(inviting_user_id, Some(user1));
 657    assert!(signup_device_id.is_none());
 658    assert_eq!(
 659        db.get_contacts(user1).await.unwrap(),
 660        [
 661            Contact::Accepted {
 662                user_id: user2,
 663                should_notify: true,
 664                busy: false,
 665            },
 666            Contact::Accepted {
 667                user_id: user3,
 668                should_notify: true,
 669                busy: false,
 670            }
 671        ]
 672    );
 673    assert_eq!(
 674        db.get_contacts(user3).await.unwrap(),
 675        [Contact::Accepted {
 676            user_id: user1,
 677            should_notify: false,
 678            busy: false,
 679        }]
 680    );
 681    assert!(db.has_contact(user1, user3).await.unwrap());
 682    assert!(db.has_contact(user3, user1).await.unwrap());
 683    assert_eq!(
 684        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
 685        3
 686    );
 687
 688    // Trying to reedem the code for the third time results in an error.
 689    db.create_invite_from_code(
 690        &invite_code,
 691        "user4@example.com",
 692        Some("user-4-device-id"),
 693        true,
 694    )
 695    .await
 696    .unwrap_err();
 697
 698    // Invite count can be updated after the code has been created.
 699    db.set_invite_count_for_user(user1, 2).await.unwrap();
 700    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 701    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
 702    assert_eq!(invite_count, 2);
 703
 704    // User 4 can now redeem the invite code and becomes a contact of user 1.
 705    let user4_invite = db
 706        .create_invite_from_code(
 707            &invite_code,
 708            "user4@example.com",
 709            Some("user-4-device-id"),
 710            true,
 711        )
 712        .await
 713        .unwrap();
 714    let user4 = db
 715        .create_user_from_invite(
 716            &user4_invite,
 717            NewUserParams {
 718                github_login: "user-4".into(),
 719                github_user_id: 4,
 720                invite_count: 5,
 721            },
 722        )
 723        .await
 724        .unwrap()
 725        .unwrap()
 726        .user_id;
 727
 728    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 729    assert_eq!(invite_count, 1);
 730    assert_eq!(
 731        db.get_contacts(user1).await.unwrap(),
 732        [
 733            Contact::Accepted {
 734                user_id: user2,
 735                should_notify: true,
 736                busy: false,
 737            },
 738            Contact::Accepted {
 739                user_id: user3,
 740                should_notify: true,
 741                busy: false,
 742            },
 743            Contact::Accepted {
 744                user_id: user4,
 745                should_notify: true,
 746                busy: false,
 747            }
 748        ]
 749    );
 750    assert_eq!(
 751        db.get_contacts(user4).await.unwrap(),
 752        [Contact::Accepted {
 753            user_id: user1,
 754            should_notify: false,
 755            busy: false,
 756        }]
 757    );
 758    assert!(db.has_contact(user1, user4).await.unwrap());
 759    assert!(db.has_contact(user4, user1).await.unwrap());
 760    assert_eq!(
 761        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
 762        5
 763    );
 764
 765    // An existing user cannot redeem invite codes.
 766    db.create_invite_from_code(
 767        &invite_code,
 768        "user2@example.com",
 769        Some("user-2-device-id"),
 770        true,
 771    )
 772    .await
 773    .unwrap_err();
 774    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 775    assert_eq!(invite_count, 1);
 776
 777    // A newer user can invite an existing one via a different email address
 778    // than the one they used to sign up.
 779    let user5 = db
 780        .create_user(
 781            "user5@example.com",
 782            false,
 783            NewUserParams {
 784                github_login: "user5".into(),
 785                github_user_id: 5,
 786                invite_count: 0,
 787            },
 788        )
 789        .await
 790        .unwrap()
 791        .user_id;
 792    db.set_invite_count_for_user(user5, 5).await.unwrap();
 793    let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
 794    let user5_invite_to_user1 = db
 795        .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
 796        .await
 797        .unwrap();
 798    let user1_2 = db
 799        .create_user_from_invite(
 800            &user5_invite_to_user1,
 801            NewUserParams {
 802                github_login: "user1".into(),
 803                github_user_id: 1,
 804                invite_count: 5,
 805            },
 806        )
 807        .await
 808        .unwrap()
 809        .unwrap()
 810        .user_id;
 811    assert_eq!(user1_2, user1);
 812    assert_eq!(
 813        db.get_contacts(user1).await.unwrap(),
 814        [
 815            Contact::Accepted {
 816                user_id: user2,
 817                should_notify: true,
 818                busy: false,
 819            },
 820            Contact::Accepted {
 821                user_id: user3,
 822                should_notify: true,
 823                busy: false,
 824            },
 825            Contact::Accepted {
 826                user_id: user4,
 827                should_notify: true,
 828                busy: false,
 829            },
 830            Contact::Accepted {
 831                user_id: user5,
 832                should_notify: false,
 833                busy: false,
 834            }
 835        ]
 836    );
 837    assert_eq!(
 838        db.get_contacts(user5).await.unwrap(),
 839        [Contact::Accepted {
 840            user_id: user1,
 841            should_notify: true,
 842            busy: false,
 843        }]
 844    );
 845    assert!(db.has_contact(user1, user5).await.unwrap());
 846    assert!(db.has_contact(user5, user1).await.unwrap());
 847}
 848
 849#[gpui::test]
 850async fn test_multiple_signup_overwrite() {
 851    let test_db = TestDb::postgres(build_background_executor());
 852    let db = test_db.db();
 853
 854    let email_address = "user_1@example.com".to_string();
 855
 856    let initial_signup_created_at_milliseconds = 0;
 857
 858    let initial_signup = NewSignup {
 859        email_address: email_address.clone(),
 860        platform_mac: false,
 861        platform_linux: true,
 862        platform_windows: false,
 863        editor_features: vec!["speed".into()],
 864        programming_languages: vec!["rust".into(), "c".into()],
 865        device_id: Some(format!("device_id")),
 866        added_to_mailing_list: false,
 867        created_at: Some(
 868            DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
 869        ),
 870    };
 871
 872    db.create_signup(&initial_signup).await.unwrap();
 873
 874    let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
 875
 876    assert_eq!(
 877        initial_signup_from_db.clone(),
 878        signup::Model {
 879            email_address: initial_signup.email_address,
 880            platform_mac: initial_signup.platform_mac,
 881            platform_linux: initial_signup.platform_linux,
 882            platform_windows: initial_signup.platform_windows,
 883            editor_features: Some(initial_signup.editor_features),
 884            programming_languages: Some(initial_signup.programming_languages),
 885            added_to_mailing_list: initial_signup.added_to_mailing_list,
 886            ..initial_signup_from_db
 887        }
 888    );
 889
 890    let subsequent_signup = NewSignup {
 891        email_address: email_address.clone(),
 892        platform_mac: true,
 893        platform_linux: false,
 894        platform_windows: true,
 895        editor_features: vec!["git integration".into(), "clean design".into()],
 896        programming_languages: vec!["d".into(), "elm".into()],
 897        device_id: Some(format!("different_device_id")),
 898        added_to_mailing_list: true,
 899        // subsequent signup happens next day
 900        created_at: Some(
 901            DateTime::from_timestamp_millis(
 902                initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
 903            )
 904            .unwrap(),
 905        ),
 906    };
 907
 908    db.create_signup(&subsequent_signup).await.unwrap();
 909
 910    let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
 911
 912    assert_eq!(
 913        subsequent_signup_from_db.clone(),
 914        signup::Model {
 915            platform_mac: subsequent_signup.platform_mac,
 916            platform_linux: subsequent_signup.platform_linux,
 917            platform_windows: subsequent_signup.platform_windows,
 918            editor_features: Some(subsequent_signup.editor_features),
 919            programming_languages: Some(subsequent_signup.programming_languages),
 920            device_id: subsequent_signup.device_id,
 921            added_to_mailing_list: subsequent_signup.added_to_mailing_list,
 922            // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
 923            created_at: initial_signup_from_db.created_at,
 924            ..subsequent_signup_from_db
 925        }
 926    );
 927}
 928
 929#[gpui::test]
 930async fn test_signups() {
 931    let test_db = TestDb::postgres(build_background_executor());
 932    let db = test_db.db();
 933
 934    let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
 935
 936    let all_signups = usernames
 937        .iter()
 938        .enumerate()
 939        .map(|(i, username)| NewSignup {
 940            email_address: format!("{username}@example.com"),
 941            platform_mac: true,
 942            platform_linux: i % 2 == 0,
 943            platform_windows: i % 4 == 0,
 944            editor_features: vec!["speed".into()],
 945            programming_languages: vec!["rust".into(), "c".into()],
 946            device_id: Some(format!("device_id_{i}")),
 947            added_to_mailing_list: i != 0, // One user failed to subscribe
 948            created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
 949        })
 950        .collect::<Vec<NewSignup>>();
 951
 952    // people sign up on the waitlist
 953    for signup in &all_signups {
 954        // users can sign up multiple times without issues
 955        for _ in 0..2 {
 956            db.create_signup(&signup).await.unwrap();
 957        }
 958    }
 959
 960    assert_eq!(
 961        db.get_waitlist_summary().await.unwrap(),
 962        WaitlistSummary {
 963            count: 8,
 964            mac_count: 8,
 965            linux_count: 4,
 966            windows_count: 2,
 967            unknown_count: 0,
 968        }
 969    );
 970
 971    // retrieve the next batch of signup emails to send
 972    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
 973    let addresses = signups_batch1
 974        .iter()
 975        .map(|s| &s.email_address)
 976        .collect::<Vec<_>>();
 977    assert_eq!(
 978        addresses,
 979        &[
 980            all_signups[0].email_address.as_str(),
 981            all_signups[1].email_address.as_str(),
 982            all_signups[2].email_address.as_str()
 983        ]
 984    );
 985    assert_ne!(
 986        signups_batch1[0].email_confirmation_code,
 987        signups_batch1[1].email_confirmation_code
 988    );
 989
 990    // the waitlist isn't updated until we record that the emails
 991    // were successfully sent.
 992    let signups_batch = db.get_unsent_invites(3).await.unwrap();
 993    assert_eq!(signups_batch, signups_batch1);
 994
 995    // once the emails go out, we can retrieve the next batch
 996    // of signups.
 997    db.record_sent_invites(&signups_batch1).await.unwrap();
 998    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
 999    let addresses = signups_batch2
1000        .iter()
1001        .map(|s| &s.email_address)
1002        .collect::<Vec<_>>();
1003    assert_eq!(
1004        addresses,
1005        &[
1006            all_signups[3].email_address.as_str(),
1007            all_signups[4].email_address.as_str(),
1008            all_signups[5].email_address.as_str()
1009        ]
1010    );
1011
1012    // the sent invites are excluded from the summary.
1013    assert_eq!(
1014        db.get_waitlist_summary().await.unwrap(),
1015        WaitlistSummary {
1016            count: 5,
1017            mac_count: 5,
1018            linux_count: 2,
1019            windows_count: 1,
1020            unknown_count: 0,
1021        }
1022    );
1023
1024    // user completes the signup process by providing their
1025    // github account.
1026    let NewUserResult {
1027        user_id,
1028        inviting_user_id,
1029        signup_device_id,
1030        ..
1031    } = db
1032        .create_user_from_invite(
1033            &Invite {
1034                ..signups_batch1[0].clone()
1035            },
1036            NewUserParams {
1037                github_login: usernames[0].clone(),
1038                github_user_id: 0,
1039                invite_count: 5,
1040            },
1041        )
1042        .await
1043        .unwrap()
1044        .unwrap();
1045    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1046    assert!(inviting_user_id.is_none());
1047    assert_eq!(user.github_login, usernames[0]);
1048    assert_eq!(
1049        user.email_address,
1050        Some(all_signups[0].email_address.clone())
1051    );
1052    assert_eq!(user.invite_count, 5);
1053    assert_eq!(signup_device_id.unwrap(), "device_id_0");
1054
1055    // cannot redeem the same signup again.
1056    assert!(db
1057        .create_user_from_invite(
1058            &Invite {
1059                email_address: signups_batch1[0].email_address.clone(),
1060                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1061            },
1062            NewUserParams {
1063                github_login: "some-other-github_account".into(),
1064                github_user_id: 1,
1065                invite_count: 5,
1066            },
1067        )
1068        .await
1069        .unwrap()
1070        .is_none());
1071
1072    // cannot redeem a signup with the wrong confirmation code.
1073    db.create_user_from_invite(
1074        &Invite {
1075            email_address: signups_batch1[1].email_address.clone(),
1076            email_confirmation_code: "the-wrong-code".to_string(),
1077        },
1078        NewUserParams {
1079            github_login: usernames[1].clone(),
1080            github_user_id: 2,
1081            invite_count: 5,
1082        },
1083    )
1084    .await
1085    .unwrap_err();
1086}
1087
1088fn build_background_executor() -> Arc<Background> {
1089    Deterministic::new(0).build_background()
1090}