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