chorus 0.20.0

A library for interacting with multiple Spacebar-compatible Instances at once.
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use std::str::FromStr;

use chorus::gateway::{Gateway, GatewayOptions};
use chorus::types::{DeleteDisableUserSchema, IntoShared, PermissionFlags, Snowflake};
use chorus::{
    instance::{ChorusUser, Instance},
    types::{
        Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema,
        RoleCreateModifySchema, RoleObject, Shared,
    },
    UrlBundle,
};

use chrono::NaiveDate;

#[cfg(not(target_arch = "wasm32"))]
use httptest::{
    matchers::{all_of, contains, request},
    responders::{json_encoded, status_code},
    Expectation,
};

#[allow(dead_code)]
#[derive(Debug)]
pub(crate) struct TestBundle {
    pub urls: UrlBundle,
    pub user: ChorusUser,
    pub instance: Shared<Instance>,
    pub guild: Shared<Guild>,
    pub role: Shared<RoleObject>,
    pub channel: Shared<Channel>,
}

#[allow(unused)]
impl TestBundle {
    pub(crate) async fn create_user(&mut self, username: &str) -> ChorusUser {
        let register_schema = RegisterSchema {
            username: username.to_string(),
            consent: true,
            date_of_birth: Some(NaiveDate::from_str("2000-01-01").unwrap()),
            ..Default::default()
        };

        Instance::register_account(self.instance.clone(), register_schema)
            .await
            .unwrap()
    }
    pub(crate) async fn clone_user_without_gateway(&self) -> ChorusUser {
        ChorusUser {
            client_properties: Default::default(),
            belongs_to: self.user.belongs_to.clone(),
            token: self.user.token.clone(),
            mfa_token: None,
            limits: self.user.limits.clone(),
            settings: self.user.settings.clone(),
            object: self.user.object.clone(),
            gateway: Gateway::spawn(
                &self.instance.read().unwrap().urls.wss,
                GatewayOptions::default(),
            )
            .await
            .unwrap(),
        }
    }
}

/// Set up a test by creating an [Instance] and a User for a real,
/// running server at localhost:3001. Reduces Test boilerplate.
#[allow(dead_code)]
pub(crate) async fn setup() -> TestBundle {
    // So we can get logs when tests fail
    let _ = simple_logger::SimpleLogger::with_level(
        simple_logger::SimpleLogger::new(),
        log::LevelFilter::Debug,
    )
    .init();

    let instance = Instance::new("http://localhost:3001/api", None)
        .await
        .unwrap()
        .into_shared();

    // Requires the existence of the below user.
    let reg = RegisterSchema {
        username: "integrationtestuser".into(),
        consent: true,
        date_of_birth: Some(NaiveDate::from_str("2000-01-01").unwrap()),
        ..Default::default()
    };
    let guild_create_schema = GuildCreateSchema {
        name: Some("Test-Guild!".to_string()),
        ..Default::default()
    };
    let channel_create_schema = ChannelCreateSchema {
        name: "testchannel".to_string(),
        channel_type: Some(chorus::types::ChannelType::GuildText),
        nsfw: Some(false),
        flags: Some(0),
        default_thread_rate_limit_per_user: Some(0),
        ..Default::default()
    };
    let mut user = Instance::register_account(instance.clone(), reg)
        .await
        .unwrap();
    let guild = Guild::create(&mut user, guild_create_schema).await.unwrap();
    let channel = Channel::create(&mut user, guild.id, None, channel_create_schema)
        .await
        .unwrap();

    let role_create_schema: chorus::types::RoleCreateModifySchema = RoleCreateModifySchema {
        name: Some("Bundle role".to_string()),
        permissions: PermissionFlags::from_bits(8), // Administrator permissions
        hoist: Some(true),
        icon: None,
        unicode_emoji: Some("".to_string()),
        mentionable: Some(true),
        position: None,
        color: None,
    };
    let role = chorus::types::RoleObject::create(&mut user, guild.id, role_create_schema)
        .await
        .unwrap();

    let urls = UrlBundle::new(
        "http://localhost:3001/api",
        "http://localhost:3001/api",
        "ws://localhost:3001/",
        "http://localhost:3001",
    );
    TestBundle {
        urls,
        user,
        instance,
        guild: guild.into_shared(),
        role: role.into_shared(),
        channel: channel.into_shared(),
    }
}

/// Set up a test by creating an [Instance] and a User for a mocked
/// server with httptest. Reduces Test boilerplate.
///
/// Note: httptest does not work on wasm!
///
/// This test server will always provide snowflake ids as 123456789101112131
/// and auth tokens as "faketoken"
#[allow(dead_code)]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn setup_with_mock_server(server: &httptest::Server) -> TestBundle {
    // So we can get logs when tests fail
    let _ = simple_logger::SimpleLogger::with_level(
        simple_logger::SimpleLogger::new(),
        log::LevelFilter::Debug,
    )
    .init();

    let instance = Instance::new(server.url_str("/api").as_str(), None)
        .await
        .unwrap()
        .into_shared();

    // Requires the existence of the below user.
    let reg = RegisterSchema {
        username: "integrationtestuser".into(),
        consent: true,
        date_of_birth: Some(NaiveDate::from_str("2000-01-01").unwrap()),
        ..Default::default()
    };
    let user = Instance::register_account(instance.clone(), reg)
        .await
        .unwrap();

    let guild = Guild {
        id: Snowflake(123456789101112131),
        name: Some("Test-Guild!".to_string()),
        ..Default::default()
    };

    let channel = Channel {
        id: Snowflake(123456789101112131),
        name: Some("testchannel".to_string()),
        channel_type: chorus::types::ChannelType::GuildText,
        nsfw: Some(false),
        flags: Some(0),
        default_thread_rate_limit_per_user: Some(0),
        ..Default::default()
    };

    let role = chorus::types::RoleObject {
        id: Snowflake(123456789101112131),
        name: "Bundle role".to_string(),
        permissions: PermissionFlags::from_bits(8).unwrap(),
        hoist: true,
        unicode_emoji: Some(String::new()),
        mentionable: true,
        ..Default::default()
    };

    let urls = instance.read().unwrap().urls.clone();

    TestBundle {
        urls,
        user,
        instance,
        guild: guild.into_shared(),
        role: role.into_shared(),
        channel: channel.into_shared(),
    }
}

// Teardown method to clean up after a test.
#[allow(dead_code)]
pub(crate) async fn teardown(mut bundle: TestBundle) {
    let id = bundle.guild.read().unwrap().id;
    Guild::delete(&mut bundle.user, id).await.unwrap();
    bundle
        .user
        .delete(DeleteDisableUserSchema { password: None })
        .await
        .unwrap()
}

/// Creates a mock http server at localhost:3001 with the basic routes
/// needed to run TestBundle setup and teardown
///
/// Note: httptest does not work on wasm!
///
/// This test server will always provide snowflake ids as 123456789101112131
/// and auth tokens as "faketoken"
#[allow(dead_code)]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn create_mock_server() -> httptest::Server {
    let server = httptest::Server::run();

    let api_url = server.url("/api");
    let cdn_url = server.url("/cdn");

    // Just redirect it to the one we're running for spacebar tests
    // We're using this just for the api anyway, so it can break after identifying
    let gateway_url = "ws://localhost:3001";

    // Mock the instance/domains endpoint, so we can create from a single url
    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/policies/instance/domains")
        ])
        .times(0..100)
        .respond_with(json_encoded(
            chorus::types::types::domains_configuration::Domains {
                api_endpoint: api_url.to_string(),
                cdn: cdn_url.to_string(),
                gateway: gateway_url.to_string(),
                default_api_version: "v9".to_string(),
            },
        )),
    );

    // The following routes are mocked so that login and register work:
    server.expect(
        Expectation::matching(all_of![
            request::method("POST"),
            request::path("/api/auth/register")
        ])
        .times(0..100)
        .respond_with(json_encoded(chorus::instance::Token {
            token: "faketoken".to_string(),
        })),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("POST"),
            request::path("/api/auth/login")
        ])
        .times(0..100)
        .respond_with(json_encoded(chorus::types::LoginResult {
            token: "faketoken".to_string(),
            settings: chorus::types::UserSettings {
                ..Default::default()
            }
            .into_shared(),
        })),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/users/@me"),
            request::headers(contains(("authorization", "faketoken")))
        ])
        .times(0..100)
        .respond_with(json_encoded(chorus::types::User {
            id: chorus::types::Snowflake(123456789101112131),
            username: "integrationtestuser".to_string(),
            discriminator: "1234".to_string(),
            mfa_enabled: Some(true),
            locale: Some(String::from("en-us")),
            disabled: Some(false),
            ..Default::default()
        })),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/users/@me/settings"),
            request::headers(contains(("authorization", "faketoken")))
        ])
        .times(0..100)
        .respond_with(json_encoded(chorus::types::UserSettings {
            status: chorus::types::UserStatus::Online.into_shared(),
            ..Default::default()
        })),
    );

    // The folowing routes are mocked so that teardown works:
    server.expect(
        Expectation::matching(all_of![
            request::method("POST"),
            // Can we have wildcards here?
            request::path("/api/guilds/123456789101112131/delete"),
            request::headers(contains(("authorization", "faketoken")))
        ])
        .times(0..100)
        .respond_with(status_code(200)),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("POST"),
            request::path("/api/users/@me/delete"),
            request::headers(contains(("authorization", "faketoken")))
        ])
        .times(0..100)
        .respond_with(status_code(200)),
    );

    // The following should just return a 404, and it's normal that we're getting them
    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/.well-known/spacebar")
        ])
        .times(0..100)
        .respond_with(status_code(404)),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/api/policies/instance/domains")
        ])
        .times(0..100)
        .respond_with(status_code(404)),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/policies/instance/limits")
        ])
        .times(0..100)
        .respond_with(status_code(404)),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/policies/instance/")
        ])
        .times(0..100)
        .respond_with(status_code(404)),
    );

    server.expect(
        Expectation::matching(all_of![
            request::method("GET"),
            request::path("/api/version")
        ])
        .times(0..100)
        .respond_with(status_code(404)),
    );

    server.expect(
        Expectation::matching(all_of![request::method("GET"), request::path("/api/ping")])
            .times(0..100)
            .respond_with(status_code(404)),
    );

    server
}