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!(
 670            db.get_contacts(user_1).await.unwrap(),
 671            vec![Contact::Accepted {
 672                user_id: user_1,
 673                should_notify: false
 674            }],
 675        );
 676
 677        // User requests a contact. Both users see the pending request.
 678        db.send_contact_request(user_1, user_2).await.unwrap();
 679        assert!(!db.has_contact(user_1, user_2).await.unwrap());
 680        assert!(!db.has_contact(user_2, user_1).await.unwrap());
 681        assert_eq!(
 682            db.get_contacts(user_1).await.unwrap(),
 683            &[
 684                Contact::Accepted {
 685                    user_id: user_1,
 686                    should_notify: false
 687                },
 688                Contact::Outgoing { user_id: user_2 }
 689            ],
 690        );
 691        assert_eq!(
 692            db.get_contacts(user_2).await.unwrap(),
 693            &[
 694                Contact::Incoming {
 695                    user_id: user_1,
 696                    should_notify: true
 697                },
 698                Contact::Accepted {
 699                    user_id: user_2,
 700                    should_notify: false
 701                },
 702            ]
 703        );
 704
 705        // User 2 dismisses the contact request notification without accepting or rejecting.
 706        // We shouldn't notify them again.
 707        db.dismiss_contact_notification(user_1, user_2)
 708            .await
 709            .unwrap_err();
 710        db.dismiss_contact_notification(user_2, user_1)
 711            .await
 712            .unwrap();
 713        assert_eq!(
 714            db.get_contacts(user_2).await.unwrap(),
 715            &[
 716                Contact::Incoming {
 717                    user_id: user_1,
 718                    should_notify: false
 719                },
 720                Contact::Accepted {
 721                    user_id: user_2,
 722                    should_notify: false
 723                },
 724            ]
 725        );
 726
 727        // User can't accept their own contact request
 728        db.respond_to_contact_request(user_1, user_2, true)
 729            .await
 730            .unwrap_err();
 731
 732        // User accepts a contact request. Both users see the contact.
 733        db.respond_to_contact_request(user_2, user_1, true)
 734            .await
 735            .unwrap();
 736        assert_eq!(
 737            db.get_contacts(user_1).await.unwrap(),
 738            &[
 739                Contact::Accepted {
 740                    user_id: user_1,
 741                    should_notify: false
 742                },
 743                Contact::Accepted {
 744                    user_id: user_2,
 745                    should_notify: true
 746                }
 747            ],
 748        );
 749        assert!(db.has_contact(user_1, user_2).await.unwrap());
 750        assert!(db.has_contact(user_2, user_1).await.unwrap());
 751        assert_eq!(
 752            db.get_contacts(user_2).await.unwrap(),
 753            &[
 754                Contact::Accepted {
 755                    user_id: user_1,
 756                    should_notify: false,
 757                },
 758                Contact::Accepted {
 759                    user_id: user_2,
 760                    should_notify: false,
 761                },
 762            ]
 763        );
 764
 765        // Users cannot re-request existing contacts.
 766        db.send_contact_request(user_1, user_2).await.unwrap_err();
 767        db.send_contact_request(user_2, user_1).await.unwrap_err();
 768
 769        // Users can't dismiss notifications of them accepting other users' requests.
 770        db.dismiss_contact_notification(user_2, user_1)
 771            .await
 772            .unwrap_err();
 773        assert_eq!(
 774            db.get_contacts(user_1).await.unwrap(),
 775            &[
 776                Contact::Accepted {
 777                    user_id: user_1,
 778                    should_notify: false
 779                },
 780                Contact::Accepted {
 781                    user_id: user_2,
 782                    should_notify: true,
 783                },
 784            ]
 785        );
 786
 787        // Users can dismiss notifications of other users accepting their requests.
 788        db.dismiss_contact_notification(user_1, user_2)
 789            .await
 790            .unwrap();
 791        assert_eq!(
 792            db.get_contacts(user_1).await.unwrap(),
 793            &[
 794                Contact::Accepted {
 795                    user_id: user_1,
 796                    should_notify: false
 797                },
 798                Contact::Accepted {
 799                    user_id: user_2,
 800                    should_notify: false,
 801                },
 802            ]
 803        );
 804
 805        // Users send each other concurrent contact requests and
 806        // see that they are immediately accepted.
 807        db.send_contact_request(user_1, user_3).await.unwrap();
 808        db.send_contact_request(user_3, user_1).await.unwrap();
 809        assert_eq!(
 810            db.get_contacts(user_1).await.unwrap(),
 811            &[
 812                Contact::Accepted {
 813                    user_id: user_1,
 814                    should_notify: false
 815                },
 816                Contact::Accepted {
 817                    user_id: user_2,
 818                    should_notify: false,
 819                },
 820                Contact::Accepted {
 821                    user_id: user_3,
 822                    should_notify: false
 823                },
 824            ]
 825        );
 826        assert_eq!(
 827            db.get_contacts(user_3).await.unwrap(),
 828            &[
 829                Contact::Accepted {
 830                    user_id: user_1,
 831                    should_notify: false
 832                },
 833                Contact::Accepted {
 834                    user_id: user_3,
 835                    should_notify: false
 836                }
 837            ],
 838        );
 839
 840        // User declines a contact request. Both users see that it is gone.
 841        db.send_contact_request(user_2, user_3).await.unwrap();
 842        db.respond_to_contact_request(user_3, user_2, false)
 843            .await
 844            .unwrap();
 845        assert!(!db.has_contact(user_2, user_3).await.unwrap());
 846        assert!(!db.has_contact(user_3, user_2).await.unwrap());
 847        assert_eq!(
 848            db.get_contacts(user_2).await.unwrap(),
 849            &[
 850                Contact::Accepted {
 851                    user_id: user_1,
 852                    should_notify: false
 853                },
 854                Contact::Accepted {
 855                    user_id: user_2,
 856                    should_notify: false
 857                }
 858            ]
 859        );
 860        assert_eq!(
 861            db.get_contacts(user_3).await.unwrap(),
 862            &[
 863                Contact::Accepted {
 864                    user_id: user_1,
 865                    should_notify: false
 866                },
 867                Contact::Accepted {
 868                    user_id: user_3,
 869                    should_notify: false
 870                }
 871            ],
 872        );
 873    }
 874}
 875
 876#[tokio::test(flavor = "multi_thread")]
 877async fn test_invite_codes() {
 878    let postgres = TestDb::postgres().await;
 879    let db = postgres.db();
 880    let NewUserResult { user_id: user1, .. } = db
 881        .create_user(
 882            "user1@example.com",
 883            false,
 884            NewUserParams {
 885                github_login: "user1".into(),
 886                github_user_id: 0,
 887                invite_count: 0,
 888            },
 889        )
 890        .await
 891        .unwrap();
 892
 893    // Initially, user 1 has no invite code
 894    assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
 895
 896    // Setting invite count to 0 when no code is assigned does not assign a new code
 897    db.set_invite_count_for_user(user1, 0).await.unwrap();
 898    assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
 899
 900    // User 1 creates an invite code that can be used twice.
 901    db.set_invite_count_for_user(user1, 2).await.unwrap();
 902    let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 903    assert_eq!(invite_count, 2);
 904
 905    // User 2 redeems the invite code and becomes a contact of user 1.
 906    let user2_invite = db
 907        .create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
 908        .await
 909        .unwrap();
 910    let NewUserResult {
 911        user_id: user2,
 912        inviting_user_id,
 913        signup_device_id,
 914        metrics_id,
 915    } = db
 916        .create_user_from_invite(
 917            &user2_invite,
 918            NewUserParams {
 919                github_login: "user2".into(),
 920                github_user_id: 2,
 921                invite_count: 7,
 922            },
 923        )
 924        .await
 925        .unwrap();
 926    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 927    assert_eq!(invite_count, 1);
 928    assert_eq!(inviting_user_id, Some(user1));
 929    assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
 930    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
 931    assert_eq!(
 932        db.get_contacts(user1).await.unwrap(),
 933        [
 934            Contact::Accepted {
 935                user_id: user1,
 936                should_notify: false
 937            },
 938            Contact::Accepted {
 939                user_id: user2,
 940                should_notify: true
 941            }
 942        ]
 943    );
 944    assert_eq!(
 945        db.get_contacts(user2).await.unwrap(),
 946        [
 947            Contact::Accepted {
 948                user_id: user1,
 949                should_notify: false
 950            },
 951            Contact::Accepted {
 952                user_id: user2,
 953                should_notify: false
 954            }
 955        ]
 956    );
 957    assert_eq!(
 958        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
 959        7
 960    );
 961
 962    // User 3 redeems the invite code and becomes a contact of user 1.
 963    let user3_invite = db
 964        .create_invite_from_code(&invite_code, "user3@example.com", None)
 965        .await
 966        .unwrap();
 967    let NewUserResult {
 968        user_id: user3,
 969        inviting_user_id,
 970        signup_device_id,
 971        ..
 972    } = db
 973        .create_user_from_invite(
 974            &user3_invite,
 975            NewUserParams {
 976                github_login: "user-3".into(),
 977                github_user_id: 3,
 978                invite_count: 3,
 979            },
 980        )
 981        .await
 982        .unwrap();
 983    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 984    assert_eq!(invite_count, 0);
 985    assert_eq!(inviting_user_id, Some(user1));
 986    assert!(signup_device_id.is_none());
 987    assert_eq!(
 988        db.get_contacts(user1).await.unwrap(),
 989        [
 990            Contact::Accepted {
 991                user_id: user1,
 992                should_notify: false
 993            },
 994            Contact::Accepted {
 995                user_id: user2,
 996                should_notify: true
 997            },
 998            Contact::Accepted {
 999                user_id: user3,
1000                should_notify: true
1001            }
1002        ]
1003    );
1004    assert_eq!(
1005        db.get_contacts(user3).await.unwrap(),
1006        [
1007            Contact::Accepted {
1008                user_id: user1,
1009                should_notify: false
1010            },
1011            Contact::Accepted {
1012                user_id: user3,
1013                should_notify: false
1014            },
1015        ]
1016    );
1017    assert_eq!(
1018        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
1019        3
1020    );
1021
1022    // Trying to reedem the code for the third time results in an error.
1023    db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
1024        .await
1025        .unwrap_err();
1026
1027    // Invite count can be updated after the code has been created.
1028    db.set_invite_count_for_user(user1, 2).await.unwrap();
1029    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1030    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
1031    assert_eq!(invite_count, 2);
1032
1033    // User 4 can now redeem the invite code and becomes a contact of user 1.
1034    let user4_invite = db
1035        .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
1036        .await
1037        .unwrap();
1038    let user4 = db
1039        .create_user_from_invite(
1040            &user4_invite,
1041            NewUserParams {
1042                github_login: "user-4".into(),
1043                github_user_id: 4,
1044                invite_count: 5,
1045            },
1046        )
1047        .await
1048        .unwrap()
1049        .user_id;
1050
1051    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1052    assert_eq!(invite_count, 1);
1053    assert_eq!(
1054        db.get_contacts(user1).await.unwrap(),
1055        [
1056            Contact::Accepted {
1057                user_id: user1,
1058                should_notify: false
1059            },
1060            Contact::Accepted {
1061                user_id: user2,
1062                should_notify: true
1063            },
1064            Contact::Accepted {
1065                user_id: user3,
1066                should_notify: true
1067            },
1068            Contact::Accepted {
1069                user_id: user4,
1070                should_notify: true
1071            }
1072        ]
1073    );
1074    assert_eq!(
1075        db.get_contacts(user4).await.unwrap(),
1076        [
1077            Contact::Accepted {
1078                user_id: user1,
1079                should_notify: false
1080            },
1081            Contact::Accepted {
1082                user_id: user4,
1083                should_notify: false
1084            },
1085        ]
1086    );
1087    assert_eq!(
1088        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
1089        5
1090    );
1091
1092    // An existing user cannot redeem invite codes.
1093    db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
1094        .await
1095        .unwrap_err();
1096    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1097    assert_eq!(invite_count, 1);
1098}
1099
1100#[tokio::test(flavor = "multi_thread")]
1101async fn test_signups() {
1102    let postgres = TestDb::postgres().await;
1103    let db = postgres.db();
1104
1105    // people sign up on the waitlist
1106    for i in 0..8 {
1107        db.create_signup(Signup {
1108            email_address: format!("person-{i}@example.com"),
1109            platform_mac: true,
1110            platform_linux: i % 2 == 0,
1111            platform_windows: i % 4 == 0,
1112            editor_features: vec!["speed".into()],
1113            programming_languages: vec!["rust".into(), "c".into()],
1114            device_id: Some(format!("device_id_{i}")),
1115        })
1116        .await
1117        .unwrap();
1118    }
1119
1120    assert_eq!(
1121        db.get_waitlist_summary().await.unwrap(),
1122        WaitlistSummary {
1123            count: 8,
1124            mac_count: 8,
1125            linux_count: 4,
1126            windows_count: 2,
1127        }
1128    );
1129
1130    // retrieve the next batch of signup emails to send
1131    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
1132    let addresses = signups_batch1
1133        .iter()
1134        .map(|s| &s.email_address)
1135        .collect::<Vec<_>>();
1136    assert_eq!(
1137        addresses,
1138        &[
1139            "person-0@example.com",
1140            "person-1@example.com",
1141            "person-2@example.com"
1142        ]
1143    );
1144    assert_ne!(
1145        signups_batch1[0].email_confirmation_code,
1146        signups_batch1[1].email_confirmation_code
1147    );
1148
1149    // the waitlist isn't updated until we record that the emails
1150    // were successfully sent.
1151    let signups_batch = db.get_unsent_invites(3).await.unwrap();
1152    assert_eq!(signups_batch, signups_batch1);
1153
1154    // once the emails go out, we can retrieve the next batch
1155    // of signups.
1156    db.record_sent_invites(&signups_batch1).await.unwrap();
1157    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
1158    let addresses = signups_batch2
1159        .iter()
1160        .map(|s| &s.email_address)
1161        .collect::<Vec<_>>();
1162    assert_eq!(
1163        addresses,
1164        &[
1165            "person-3@example.com",
1166            "person-4@example.com",
1167            "person-5@example.com"
1168        ]
1169    );
1170
1171    // the sent invites are excluded from the summary.
1172    assert_eq!(
1173        db.get_waitlist_summary().await.unwrap(),
1174        WaitlistSummary {
1175            count: 5,
1176            mac_count: 5,
1177            linux_count: 2,
1178            windows_count: 1,
1179        }
1180    );
1181
1182    // user completes the signup process by providing their
1183    // github account.
1184    let NewUserResult {
1185        user_id,
1186        inviting_user_id,
1187        signup_device_id,
1188        ..
1189    } = db
1190        .create_user_from_invite(
1191            &Invite {
1192                email_address: signups_batch1[0].email_address.clone(),
1193                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1194            },
1195            NewUserParams {
1196                github_login: "person-0".into(),
1197                github_user_id: 0,
1198                invite_count: 5,
1199            },
1200        )
1201        .await
1202        .unwrap();
1203    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1204    assert!(inviting_user_id.is_none());
1205    assert_eq!(user.github_login, "person-0");
1206    assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
1207    assert_eq!(user.invite_count, 5);
1208    assert_eq!(signup_device_id.unwrap(), "device_id_0");
1209
1210    // cannot redeem the same signup again.
1211    db.create_user_from_invite(
1212        &Invite {
1213            email_address: signups_batch1[0].email_address.clone(),
1214            email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1215        },
1216        NewUserParams {
1217            github_login: "some-other-github_account".into(),
1218            github_user_id: 1,
1219            invite_count: 5,
1220        },
1221    )
1222    .await
1223    .unwrap_err();
1224
1225    // cannot redeem a signup with the wrong confirmation code.
1226    db.create_user_from_invite(
1227        &Invite {
1228            email_address: signups_batch1[1].email_address.clone(),
1229            email_confirmation_code: "the-wrong-code".to_string(),
1230        },
1231        NewUserParams {
1232            github_login: "person-1".into(),
1233            github_user_id: 2,
1234            invite_count: 5,
1235        },
1236    )
1237    .await
1238    .unwrap_err();
1239}
1240
1241#[tokio::test(flavor = "multi_thread")]
1242async fn test_metrics_id() {
1243    let postgres = TestDb::postgres().await;
1244    let db = postgres.db();
1245
1246    let NewUserResult {
1247        user_id: user1,
1248        metrics_id: metrics_id1,
1249        ..
1250    } = db
1251        .create_user(
1252            "person1@example.com",
1253            false,
1254            NewUserParams {
1255                github_login: "person1".into(),
1256                github_user_id: 101,
1257                invite_count: 5,
1258            },
1259        )
1260        .await
1261        .unwrap();
1262    let NewUserResult {
1263        user_id: user2,
1264        metrics_id: metrics_id2,
1265        ..
1266    } = db
1267        .create_user(
1268            "person2@example.com",
1269            false,
1270            NewUserParams {
1271                github_login: "person2".into(),
1272                github_user_id: 102,
1273                invite_count: 5,
1274            },
1275        )
1276        .await
1277        .unwrap();
1278
1279    assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
1280    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
1281    assert_eq!(metrics_id1.len(), 36);
1282    assert_eq!(metrics_id2.len(), 36);
1283    assert_ne!(metrics_id1, metrics_id2);
1284}
1285
1286fn build_background_executor() -> Arc<Background> {
1287    Deterministic::new(0).build_background()
1288}