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")
 958        .await
 959        .unwrap();
 960    let (user2, inviter, _) = db
 961        .create_user_from_invite(
 962            &user2_invite,
 963            NewUserParams {
 964                github_login: "user2".into(),
 965                github_user_id: 2,
 966                invite_count: 7,
 967            },
 968        )
 969        .await
 970        .unwrap();
 971    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
 972    assert_eq!(invite_count, 1);
 973    assert_eq!(inviter, Some(user1));
 974    assert_eq!(
 975        db.get_contacts(user1).await.unwrap(),
 976        [
 977            Contact::Accepted {
 978                user_id: user1,
 979                should_notify: false
 980            },
 981            Contact::Accepted {
 982                user_id: user2,
 983                should_notify: true
 984            }
 985        ]
 986    );
 987    assert_eq!(
 988        db.get_contacts(user2).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: false
 997            }
 998        ]
 999    );
1000    assert_eq!(
1001        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
1002        7
1003    );
1004
1005    // User 3 redeems the invite code and becomes a contact of user 1.
1006    let user3_invite = db
1007        .create_invite_from_code(&invite_code, "u3@example.com")
1008        .await
1009        .unwrap();
1010    let (user3, inviter, _) = db
1011        .create_user_from_invite(
1012            &user3_invite,
1013            NewUserParams {
1014                github_login: "user-3".into(),
1015                github_user_id: 3,
1016                invite_count: 3,
1017            },
1018        )
1019        .await
1020        .unwrap();
1021    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1022    assert_eq!(invite_count, 0);
1023    assert_eq!(inviter, Some(user1));
1024    assert_eq!(
1025        db.get_contacts(user1).await.unwrap(),
1026        [
1027            Contact::Accepted {
1028                user_id: user1,
1029                should_notify: false
1030            },
1031            Contact::Accepted {
1032                user_id: user2,
1033                should_notify: true
1034            },
1035            Contact::Accepted {
1036                user_id: user3,
1037                should_notify: true
1038            }
1039        ]
1040    );
1041    assert_eq!(
1042        db.get_contacts(user3).await.unwrap(),
1043        [
1044            Contact::Accepted {
1045                user_id: user1,
1046                should_notify: false
1047            },
1048            Contact::Accepted {
1049                user_id: user3,
1050                should_notify: false
1051            },
1052        ]
1053    );
1054    assert_eq!(
1055        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
1056        3
1057    );
1058
1059    // Trying to reedem the code for the third time results in an error.
1060    db.create_invite_from_code(&invite_code, "u4@example.com")
1061        .await
1062        .unwrap_err();
1063
1064    // Invite count can be updated after the code has been created.
1065    db.set_invite_count_for_user(user1, 2).await.unwrap();
1066    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1067    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
1068    assert_eq!(invite_count, 2);
1069
1070    // User 4 can now redeem the invite code and becomes a contact of user 1.
1071    let user4_invite = db
1072        .create_invite_from_code(&invite_code, "u4@example.com")
1073        .await
1074        .unwrap();
1075    let (user4, _, _) = db
1076        .create_user_from_invite(
1077            &user4_invite,
1078            NewUserParams {
1079                github_login: "user-4".into(),
1080                github_user_id: 4,
1081                invite_count: 5,
1082            },
1083        )
1084        .await
1085        .unwrap();
1086
1087    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1088    assert_eq!(invite_count, 1);
1089    assert_eq!(
1090        db.get_contacts(user1).await.unwrap(),
1091        [
1092            Contact::Accepted {
1093                user_id: user1,
1094                should_notify: false
1095            },
1096            Contact::Accepted {
1097                user_id: user2,
1098                should_notify: true
1099            },
1100            Contact::Accepted {
1101                user_id: user3,
1102                should_notify: true
1103            },
1104            Contact::Accepted {
1105                user_id: user4,
1106                should_notify: true
1107            }
1108        ]
1109    );
1110    assert_eq!(
1111        db.get_contacts(user4).await.unwrap(),
1112        [
1113            Contact::Accepted {
1114                user_id: user1,
1115                should_notify: false
1116            },
1117            Contact::Accepted {
1118                user_id: user4,
1119                should_notify: false
1120            },
1121        ]
1122    );
1123    assert_eq!(
1124        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
1125        5
1126    );
1127
1128    // An existing user cannot redeem invite codes.
1129    db.create_invite_from_code(&invite_code, "u2@example.com")
1130        .await
1131        .unwrap_err();
1132    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
1133    assert_eq!(invite_count, 1);
1134}
1135
1136#[tokio::test(flavor = "multi_thread")]
1137async fn test_signups() {
1138    let postgres = TestDb::postgres().await;
1139    let db = postgres.db();
1140
1141    // people sign up on the waitlist
1142    for i in 0..8 {
1143        db.create_signup(Signup {
1144            email_address: format!("person-{i}@example.com"),
1145            platform_mac: true,
1146            platform_linux: i % 2 == 0,
1147            platform_windows: i % 4 == 0,
1148            editor_features: vec!["speed".into()],
1149            programming_languages: vec!["rust".into(), "c".into()],
1150            device_id: format!("device_id_{i}"),
1151        })
1152        .await
1153        .unwrap();
1154    }
1155
1156    assert_eq!(
1157        db.get_waitlist_summary().await.unwrap(),
1158        WaitlistSummary {
1159            count: 8,
1160            mac_count: 8,
1161            linux_count: 4,
1162            windows_count: 2,
1163        }
1164    );
1165
1166    // retrieve the next batch of signup emails to send
1167    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
1168    let addresses = signups_batch1
1169        .iter()
1170        .map(|s| &s.email_address)
1171        .collect::<Vec<_>>();
1172    assert_eq!(
1173        addresses,
1174        &[
1175            "person-0@example.com",
1176            "person-1@example.com",
1177            "person-2@example.com"
1178        ]
1179    );
1180    assert_ne!(
1181        signups_batch1[0].email_confirmation_code,
1182        signups_batch1[1].email_confirmation_code
1183    );
1184
1185    // the waitlist isn't updated until we record that the emails
1186    // were successfully sent.
1187    let signups_batch = db.get_unsent_invites(3).await.unwrap();
1188    assert_eq!(signups_batch, signups_batch1);
1189
1190    // once the emails go out, we can retrieve the next batch
1191    // of signups.
1192    db.record_sent_invites(&signups_batch1).await.unwrap();
1193    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
1194    let addresses = signups_batch2
1195        .iter()
1196        .map(|s| &s.email_address)
1197        .collect::<Vec<_>>();
1198    assert_eq!(
1199        addresses,
1200        &[
1201            "person-3@example.com",
1202            "person-4@example.com",
1203            "person-5@example.com"
1204        ]
1205    );
1206
1207    // the sent invites are excluded from the summary.
1208    assert_eq!(
1209        db.get_waitlist_summary().await.unwrap(),
1210        WaitlistSummary {
1211            count: 5,
1212            mac_count: 5,
1213            linux_count: 2,
1214            windows_count: 1,
1215        }
1216    );
1217
1218    // user completes the signup process by providing their
1219    // github account.
1220    let (user_id, inviter_id, signup_device_id) = db
1221        .create_user_from_invite(
1222            &Invite {
1223                email_address: signups_batch1[0].email_address.clone(),
1224                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1225            },
1226            NewUserParams {
1227                github_login: "person-0".into(),
1228                github_user_id: 0,
1229                invite_count: 5,
1230            },
1231        )
1232        .await
1233        .unwrap();
1234    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
1235    assert!(inviter_id.is_none());
1236    assert_eq!(user.github_login, "person-0");
1237    assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
1238    assert_eq!(user.invite_count, 5);
1239    assert_eq!(signup_device_id, "device_id_0");
1240
1241    // cannot redeem the same signup again.
1242    db.create_user_from_invite(
1243        &Invite {
1244            email_address: signups_batch1[0].email_address.clone(),
1245            email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
1246        },
1247        NewUserParams {
1248            github_login: "some-other-github_account".into(),
1249            github_user_id: 1,
1250            invite_count: 5,
1251        },
1252    )
1253    .await
1254    .unwrap_err();
1255
1256    // cannot redeem a signup with the wrong confirmation code.
1257    db.create_user_from_invite(
1258        &Invite {
1259            email_address: signups_batch1[1].email_address.clone(),
1260            email_confirmation_code: "the-wrong-code".to_string(),
1261        },
1262        NewUserParams {
1263            github_login: "person-1".into(),
1264            github_user_id: 2,
1265            invite_count: 5,
1266        },
1267    )
1268    .await
1269    .unwrap_err();
1270}
1271
1272fn build_background_executor() -> Arc<Background> {
1273    Deterministic::new(0).build_background()
1274}