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        .unwrap();
 857    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 858    assert_eq!(invite_count, 1);
 859    assert_eq!(inviting_user_id, Some(user1));
 860    assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
 861    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
 862    assert_eq!(
 863        db.get_contacts(user1).await.unwrap(),
 864        [Contact::Accepted {
 865            user_id: user2,
 866            should_notify: true
 867        }]
 868    );
 869    assert_eq!(
 870        db.get_contacts(user2).await.unwrap(),
 871        [Contact::Accepted {
 872            user_id: user1,
 873            should_notify: false
 874        }]
 875    );
 876    assert_eq!(
 877        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
 878        7
 879    );
 880
 881    // User 3 redeems the invite code and becomes a contact of user 1.
 882    let user3_invite = db
 883        .create_invite_from_code(&invite_code, "user3@example.com", None)
 884        .await
 885        .unwrap();
 886    let NewUserResult {
 887        user_id: user3,
 888        inviting_user_id,
 889        signup_device_id,
 890        ..
 891    } = db
 892        .create_user_from_invite(
 893            &user3_invite,
 894            NewUserParams {
 895                github_login: "user-3".into(),
 896                github_user_id: 3,
 897                invite_count: 3,
 898            },
 899        )
 900        .await
 901        .unwrap()
 902        .unwrap();
 903    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 904    assert_eq!(invite_count, 0);
 905    assert_eq!(inviting_user_id, Some(user1));
 906    assert!(signup_device_id.is_none());
 907    assert_eq!(
 908        db.get_contacts(user1).await.unwrap(),
 909        [
 910            Contact::Accepted {
 911                user_id: user2,
 912                should_notify: true
 913            },
 914            Contact::Accepted {
 915                user_id: user3,
 916                should_notify: true
 917            }
 918        ]
 919    );
 920    assert_eq!(
 921        db.get_contacts(user3).await.unwrap(),
 922        [Contact::Accepted {
 923            user_id: user1,
 924            should_notify: false
 925        }]
 926    );
 927    assert_eq!(
 928        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
 929        3
 930    );
 931
 932    // Trying to reedem the code for the third time results in an error.
 933    db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
 934        .await
 935        .unwrap_err();
 936
 937    // Invite count can be updated after the code has been created.
 938    db.set_invite_count_for_user(user1, 2).await.unwrap();
 939    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 940    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
 941    assert_eq!(invite_count, 2);
 942
 943    // User 4 can now redeem the invite code and becomes a contact of user 1.
 944    let user4_invite = db
 945        .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
 946        .await
 947        .unwrap();
 948    let user4 = db
 949        .create_user_from_invite(
 950            &user4_invite,
 951            NewUserParams {
 952                github_login: "user-4".into(),
 953                github_user_id: 4,
 954                invite_count: 5,
 955            },
 956        )
 957        .await
 958        .unwrap()
 959        .unwrap()
 960        .user_id;
 961
 962    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 963    assert_eq!(invite_count, 1);
 964    assert_eq!(
 965        db.get_contacts(user1).await.unwrap(),
 966        [
 967            Contact::Accepted {
 968                user_id: user2,
 969                should_notify: true
 970            },
 971            Contact::Accepted {
 972                user_id: user3,
 973                should_notify: true
 974            },
 975            Contact::Accepted {
 976                user_id: user4,
 977                should_notify: true
 978            }
 979        ]
 980    );
 981    assert_eq!(
 982        db.get_contacts(user4).await.unwrap(),
 983        [Contact::Accepted {
 984            user_id: user1,
 985            should_notify: false
 986        }]
 987    );
 988    assert_eq!(
 989        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
 990        5
 991    );
 992
 993    // An existing user cannot redeem invite codes.
 994    db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
 995        .await
 996        .unwrap_err();
 997    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 998    assert_eq!(invite_count, 1);
 999}
1000
1001#[tokio::test(flavor = "multi_thread")]
1002async fn test_signups() {
1003    let postgres = TestDb::postgres().await;
1004    let db = postgres.db();
1005
1006    // people sign up on the waitlist
1007    for i in 0..8 {
1008        db.create_signup(Signup {
1009            email_address: format!("person-{i}@example.com"),
1010            platform_mac: true,
1011            platform_linux: i % 2 == 0,
1012            platform_windows: i % 4 == 0,
1013            editor_features: vec!["speed".into()],
1014            programming_languages: vec!["rust".into(), "c".into()],
1015            device_id: Some(format!("device_id_{i}")),
1016        })
1017        .await
1018        .unwrap();
1019    }
1020
1021    assert_eq!(
1022        db.get_waitlist_summary().await.unwrap(),
1023        WaitlistSummary {
1024            count: 8,
1025            mac_count: 8,
1026            linux_count: 4,
1027            windows_count: 2,
1028            unknown_count: 0,
1029        }
1030    );
1031
1032    // retrieve the next batch of signup emails to send
1033    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
1034    let addresses = signups_batch1
1035        .iter()
1036        .map(|s| &s.email_address)
1037        .collect::<Vec<_>>();
1038    assert_eq!(
1039        addresses,
1040        &[
1041            "person-0@example.com",
1042            "person-1@example.com",
1043            "person-2@example.com"
1044        ]
1045    );
1046    assert_ne!(
1047        signups_batch1[0].email_confirmation_code,
1048        signups_batch1[1].email_confirmation_code
1049    );
1050
1051    // the waitlist isn't updated until we record that the emails
1052    // were successfully sent.
1053    let signups_batch = db.get_unsent_invites(3).await.unwrap();
1054    assert_eq!(signups_batch, signups_batch1);
1055
1056    // once the emails go out, we can retrieve the next batch
1057    // of signups.
1058    db.record_sent_invites(&signups_batch1).await.unwrap();
1059    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
1060    let addresses = signups_batch2
1061        .iter()
1062        .map(|s| &s.email_address)
1063        .collect::<Vec<_>>();
1064    assert_eq!(
1065        addresses,
1066        &[
1067            "person-3@example.com",
1068            "person-4@example.com",
1069            "person-5@example.com"
1070        ]
1071    );
1072
1073    // the sent invites are excluded from the summary.
1074    assert_eq!(
1075        db.get_waitlist_summary().await.unwrap(),
1076        WaitlistSummary {
1077            count: 5,
1078            mac_count: 5,
1079            linux_count: 2,
1080            windows_count: 1,
1081            unknown_count: 0,
1082        }
1083    );
1084
1085    // user completes the signup process by providing their
1086    // github account.
1087    let NewUserResult {
1088        user_id,
1089        inviting_user_id,
1090        signup_device_id,
1091        ..
1092    } = db
1093        .create_user_from_invite(
1094            &Invite {
1095                email_address: signups_batch1[0].email_address.clone(),
1096                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1097            },
1098            NewUserParams {
1099                github_login: "person-0".into(),
1100                github_user_id: 0,
1101                invite_count: 5,
1102            },
1103        )
1104        .await
1105        .unwrap()
1106        .unwrap();
1107    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1108    assert!(inviting_user_id.is_none());
1109    assert_eq!(user.github_login, "person-0");
1110    assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
1111    assert_eq!(user.invite_count, 5);
1112    assert_eq!(signup_device_id.unwrap(), "device_id_0");
1113
1114    // cannot redeem the same signup again.
1115    assert!(db
1116        .create_user_from_invite(
1117            &Invite {
1118                email_address: signups_batch1[0].email_address.clone(),
1119                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1120            },
1121            NewUserParams {
1122                github_login: "some-other-github_account".into(),
1123                github_user_id: 1,
1124                invite_count: 5,
1125            },
1126        )
1127        .await
1128        .unwrap()
1129        .is_none());
1130
1131    // cannot redeem a signup with the wrong confirmation code.
1132    db.create_user_from_invite(
1133        &Invite {
1134            email_address: signups_batch1[1].email_address.clone(),
1135            email_confirmation_code: "the-wrong-code".to_string(),
1136        },
1137        NewUserParams {
1138            github_login: "person-1".into(),
1139            github_user_id: 2,
1140            invite_count: 5,
1141        },
1142    )
1143    .await
1144    .unwrap_err();
1145}
1146
1147#[tokio::test(flavor = "multi_thread")]
1148async fn test_metrics_id() {
1149    let postgres = TestDb::postgres().await;
1150    let db = postgres.db();
1151
1152    let NewUserResult {
1153        user_id: user1,
1154        metrics_id: metrics_id1,
1155        ..
1156    } = db
1157        .create_user(
1158            "person1@example.com",
1159            false,
1160            NewUserParams {
1161                github_login: "person1".into(),
1162                github_user_id: 101,
1163                invite_count: 5,
1164            },
1165        )
1166        .await
1167        .unwrap();
1168    let NewUserResult {
1169        user_id: user2,
1170        metrics_id: metrics_id2,
1171        ..
1172    } = db
1173        .create_user(
1174            "person2@example.com",
1175            false,
1176            NewUserParams {
1177                github_login: "person2".into(),
1178                github_user_id: 102,
1179                invite_count: 5,
1180            },
1181        )
1182        .await
1183        .unwrap();
1184
1185    assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
1186    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
1187    assert_eq!(metrics_id1.len(), 36);
1188    assert_eq!(metrics_id2.len(), 36);
1189    assert_ne!(metrics_id1, metrics_id2);
1190}
1191
1192fn build_background_executor() -> Arc<Background> {
1193    Deterministic::new(0).build_background()
1194}