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