db_tests.rs

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