diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index a6abe43..0cf6fd9 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -45,34 +45,34 @@ jobs: cargo build --verbose --all-features cargo test --verbose --all-features fi - wasm-safari: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Clone spacebar server - run: | - git clone https://github.com/bitfl0wer/server.git - - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: 'npm' - cache-dependency-path: server/package-lock.json - - name: Prepare and start Spacebar server - run: | - npm install - npm run setup - npm run start & - working-directory: ./server - - uses: Swatinem/rust-cache@v2 - with: - cache-all-crates: "true" - prefix-key: "macos" - - name: Run WASM tests with Safari, Firefox, Chrome - run: | - rustup target add wasm32-unknown-unknown - curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash - cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force - SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" + # wasm-safari: + # runs-on: macos-latest + # steps: + # - uses: actions/checkout@v4 + # - name: Clone spacebar server + # run: | + # git clone https://github.com/bitfl0wer/server.git + # - uses: actions/setup-node@v3 + # with: + # node-version: 18 + # cache: 'npm' + # cache-dependency-path: server/package-lock.json + # - name: Prepare and start Spacebar server + # run: | + # npm install + # npm run setup + # npm run start & + # working-directory: ./server + # - uses: Swatinem/rust-cache@v2 + # with: + # cache-all-crates: "true" + # prefix-key: "macos-safari" + # - name: Run WASM tests with Safari, Firefox, Chrome + # run: | + # rustup target add wasm32-unknown-unknown + # curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + # cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force + # SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" --no-fail-fast wasm-gecko: runs-on: macos-latest steps: diff --git a/README.md b/README.md index 711a1d4..a494eb9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Discord]][Discord-invite] [![Build][build-shield]][build-url] [![Coverage][coverage-shield]][coverage-url] -[![Contributors][contributors-shield]][contributors-url]
@@ -16,12 +15,12 @@


- Explore the docs » + Explore the docs »

Report Bug · - Request Feature + crates.io · Join Discord

@@ -29,20 +28,23 @@ -Chorus is a Rust library that allows developers to interact with multiple Spacebar-compatible APIs and Gateways (Including -Discord.com) simultaneously. The library provides a simple and efficient way to communicate with these services, making it easier for developers to build applications that rely on them. Chorus is open-source and welcomes contributions from the community. +Chorus is a Rust library which poses as an API wrapper for [Spacebar Chat](https://github.com/spacebarchat/) +and Discord. It is designed to be easy to use, and to be compatible with both Discord and Spacebar Chat. + +You can establish as many connections to as many servers as you want, and you can use them all at the same time. ## A Tour of Chorus -Chorus combines all the required functionalities of a user-centric Spacebar library into one package. The library -handles a lot of things for you, such as rate limiting, authentication, and more. This means that you can focus on -building your application, instead of worrying about the underlying implementation details. +Chorus combines all the required functionalities of a user-centric Spacebar library into one package. +The library handles various aspects on your behalf, such as rate limiting, authentication and maintaining +a WebSocket connection to the Gateway. This means that you can focus on building your application, +instead of worrying about the underlying implementation details. To get started with Chorus, import it into your project by adding the following to your `Cargo.toml` file: ```toml [dependencies] -chorus = "0" +chorus = "0.12.0" ``` ### Establishing a Connection diff --git a/examples/instance.rs b/examples/instance.rs index d2a042f..b8f8518 100644 --- a/examples/instance.rs +++ b/examples/instance.rs @@ -8,7 +8,7 @@ async fn main() { "wss://example.com/".to_string(), "https://example.com/cdn".to_string(), ); - let instance = Instance::new(bundle, true) + let instance = Instance::new(bundle) .await .expect("Failed to connect to the Spacebar server"); dbg!(instance.instance_info); diff --git a/examples/login.rs b/examples/login.rs index b06eade..18b5db4 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -9,7 +9,7 @@ async fn main() { "wss://example.com/".to_string(), "https://example.com/cdn".to_string(), ); - let instance = Instance::new(bundle, true) + let mut instance = Instance::new(bundle) .await .expect("Failed to connect to the Spacebar server"); // Assume, you already have an account created on this instance. Registering an account works diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 1d9fc8a..ff99be8 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -14,7 +14,7 @@ impl Instance { /// /// # Reference /// See - pub async fn login_account(mut self, login_schema: LoginSchema) -> ChorusResult { + pub async fn login_account(&mut self, login_schema: LoginSchema) -> ChorusResult { let endpoint_url = self.urls.api.clone() + "/auth/login"; let chorus_request = ChorusRequest { request: Client::new() diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index 2ea7d57..aa0b483 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -19,7 +19,7 @@ impl Instance { /// # Reference /// See pub async fn register_account( - mut self, + &mut self, register_schema: RegisterSchema, ) -> ChorusResult { let endpoint_url = self.urls.api.clone() + "/auth/register"; @@ -43,7 +43,7 @@ impl Instance { self.limits_information.as_mut().unwrap().ratelimits = shell.limits.unwrap(); } let user_object = self.get_user(token.clone(), None).await.unwrap(); - let settings = ChorusUser::get_settings(&token, &self.urls.api.clone(), &mut self).await?; + let settings = ChorusUser::get_settings(&token, &self.urls.api.clone(), self).await?; let mut identify = GatewayIdentifyPayload::common(); let gateway: GatewayHandle = Gateway::spawn(self.urls.wss.clone()).await.unwrap(); identify.token = token.clone(); diff --git a/src/errors.rs b/src/errors.rs index 4099a6b..c20ac64 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -44,6 +44,18 @@ custom_error! { InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}" } +impl From for ChorusError { + fn from(value: reqwest::Error) -> Self { + ChorusError::RequestFailed { + url: match value.url() { + Some(url) => url.to_string(), + None => "None".to_string(), + }, + error: value.to_string(), + } + } +} + custom_error! { #[derive(PartialEq, Eq)] pub ObserverError diff --git a/src/instance.rs b/src/instance.rs index 4ce4338..cd9eb76 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -12,44 +12,85 @@ use crate::errors::ChorusResult; use crate::gateway::{Gateway, GatewayHandle}; use crate::ratelimiter::ChorusRequest; use crate::types::types::subconfigs::limits::rates::RateLimits; -use crate::types::{GeneralConfiguration, Limit, LimitType, User, UserSettings}; +use crate::types::{ + GeneralConfiguration, Limit, LimitType, LimitsConfiguration, User, UserSettings, +}; use crate::UrlBundle; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] /// The [`Instance`]; what you will be using to perform all sorts of actions on the Spacebar server. /// If `limits_information` is `None`, then the instance will not be rate limited. pub struct Instance { pub urls: UrlBundle, pub instance_info: GeneralConfiguration, pub limits_information: Option, + #[serde(skip)] pub client: Client, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +impl PartialEq for Instance { + fn eq(&self, other: &Self) -> bool { + self.urls == other.urls + && self.instance_info == other.instance_info + && self.limits_information == other.limits_information + } +} + +impl Eq for Instance {} + +impl std::hash::Hash for Instance { + fn hash(&self, state: &mut H) { + self.urls.hash(state); + self.instance_info.hash(state); + if let Some(inf) = &self.limits_information { + inf.hash(state); + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq)] pub struct LimitsInformation { pub ratelimits: HashMap, pub configuration: RateLimits, } +impl std::hash::Hash for LimitsInformation { + fn hash(&self, state: &mut H) { + for (k, v) in self.ratelimits.iter() { + k.hash(state); + v.hash(state); + } + self.configuration.hash(state); + } +} + +impl PartialEq for LimitsInformation { + fn eq(&self, other: &Self) -> bool { + self.ratelimits.iter().eq(other.ratelimits.iter()) + && self.configuration == other.configuration + } +} + impl Instance { - /// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether or not to automatically use rate limits. - pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult { - let limits_information; - if limited { - let limits_configuration = ChorusRequest::get_limits_config(&urls.api).await?.rate; - let limits = ChorusRequest::limits_config_to_hashmap(&limits_configuration); - limits_information = Some(LimitsInformation { + /// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether Chorus will track and enforce rate limits for this instance. + pub async fn new(urls: UrlBundle) -> ChorusResult { + let is_limited: Option = Instance::is_limited(&urls.api).await?; + let limit_information; + + if let Some(limits_configuration) = is_limited { + let limits = ChorusRequest::limits_config_to_hashmap(&limits_configuration.rate); + limit_information = Some(LimitsInformation { ratelimits: limits, - configuration: limits_configuration, + configuration: limits_configuration.rate, }); } else { - limits_information = None; + limit_information = None } let mut instance = Instance { urls: urls.clone(), // Will be overwritten in the next step instance_info: GeneralConfiguration::default(), - limits_information, + limits_information: limit_information, client: Client::new(), }; instance.instance_info = match instance.general_configuration_schema().await { @@ -61,12 +102,39 @@ impl Instance { }; Ok(instance) } + pub(crate) fn clone_limits_if_some(&self) -> Option> { if self.limits_information.is_some() { return Some(self.limits_information.as_ref().unwrap().ratelimits.clone()); } None } + + /// Creates a new [`Instance`] by trying to get the [relevant instance urls](UrlBundle) from a root url. + /// Shorthand for `Instance::new(UrlBundle::from_root_domain(root_domain).await?)`. + /// + /// If `limited` is `true`, then Chorus will track and enforce rate limits for this instance. + pub async fn from_root_url(root_url: &str) -> ChorusResult { + let urls = UrlBundle::from_root_url(root_url).await?; + Instance::new(urls).await + } + + pub async fn is_limited(api_url: &str) -> ChorusResult> { + let api_url = UrlBundle::parse_url(api_url.to_string()); + let client = Client::new(); + let request = client + .get(format!("{}/policies/instance/limits", &api_url)) + .header(http::header::ACCEPT, "application/json") + .build()?; + let resp = match client.execute(request).await { + Ok(response) => response, + Err(_) => return Ok(None), + }; + match resp.json::().await { + Ok(limits) => Ok(Some(limits)), + Err(_) => Ok(None), + } + } } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -93,6 +161,16 @@ pub struct ChorusUser { pub gateway: GatewayHandle, } +impl PartialEq for ChorusUser { + fn eq(&self, other: &Self) -> bool { + self.token == other.token + && self.limits == other.limits + && self.gateway.url == other.gateway.url + } +} + +impl Eq for ChorusUser {} + impl ChorusUser { pub fn token(&self) -> String { self.token.clone() diff --git a/src/lib.rs b/src/lib.rs index 47bbaab..80374fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,94 @@ -//! A library for interacting with one or multiple Spacebar-compatible APIs and Gateways. -//! -//! # About -//!Chorus is a Rust library that allows developers to interact with multiple Spacebar-compatible APIs and Gateways simultaneously. The library provides a simple and efficient way to communicate with these services, making it easier for developers to build applications that rely on them. Chorus is open-source and welcomes contributions from the community. +/*! +Chorus combines all the required functionalities of a user-centric Spacebar library into one package. +The library handles various aspects on your behalf, such as rate limiting, authentication and maintaining +a WebSocket connection to the Gateway. This means that you can focus on building your application, +instead of worrying about the underlying implementation details. + +### Establishing a Connection + +To connect to a Spacebar compatible server, you need to create an [`Instance`](https://docs.rs/chorus/latest/chorus/instance/struct.Instance.html) like this: + +```rs +use chorus::instance::Instance; +use chorus::UrlBundle; + +#[tokio::main] +async fn main() { + let bundle = UrlBundle::new( + "https://example.com/api".to_string(), + "wss://example.com/".to_string(), + "https://example.com/cdn".to_string(), + ); + let instance = Instance::new(bundle, true) + .await + .expect("Failed to connect to the Spacebar server"); + // You can create as many instances of `Instance` as you want, but each `Instance` should likely be unique. + dbg!(instance.instance_info); + dbg!(instance.limits_information); +} +``` + +This Instance can now be used to log in, register and from there on, interact with the server in all sorts of ways. + +### Logging In + +Logging in correctly provides you with an instance of [`ChorusUser`](https://docs.rs/chorus/latest/chorus/instance/struct.ChorusUser.html), with which you can interact with the server and +manipulate the account. Assuming you already have an account on the server, you can log in like this: + +```rs +use chorus::types::LoginSchema; +// Assume, you already have an account created on this instance. Registering an account works +// the same way, but you'd use the Register-specific Structs and methods instead. +let login_schema = LoginSchema { + login: "user@example.com".to_string(), + password: "Correct-Horse-Battery-Staple".to_string(), + ..Default::default() +}; +// Each user connects to the Gateway. The Gateway connection lives on a seperate thread. Depending on +// the runtime feature you choose, this can potentially take advantage of all of your computers' threads. +let user = instance + .login_account(login_schema) + .await + .expect("An error occurred during the login process"); +dbg!(user.belongs_to); +dbg!(&user.object.read().unwrap().username); +``` + +## Supported Platforms + +All major desktop operating systems (Windows, macOS (aarch64/x86_64), Linux (aarch64/x86_64)) are supported. +`wasm32-unknown-unknown` is a supported compilation target on versions `0.12.0` and up. This allows you to use +Chorus in your browser, or in any other environment that supports WebAssembly. + +We recommend checking out the examples directory, as well as the documentation for more information. + +## MSRV (Minimum Supported Rust Version) + +Rust **1.67.1**. This number might change at any point while Chorus is not yet at version 1.0.0. + +## Development Setup + +Make sure that you have at least Rust 1.67.1 installed. You can check your Rust version by running `cargo --version` +in your terminal. To compile for `wasm32-unknown-unknown`, you need to install the `wasm32-unknown-unknown` target. +You can do this by running `rustup target add wasm32-unknown-unknown`. + +### Testing + +In general, the tests will require you to run a local instance of the Spacebar server. You can find instructions on how +to do that [here](https://docs.spacebar.chat/setup/server/). You can find a pre-configured version of the server +[here](https://github.com/bitfl0wer/server). It is recommended to use the pre-configured version, as certain things +like "proxy connection checking" are already disabled on this version, which otherwise might break tests. + +### wasm + +To test for wasm, you will need to `cargo install wasm-pack`. You can then run +`wasm-pack test -- --headless -- --target wasm32-unknown-unknown --features="rt, client" --no-default-features` +to run the tests for wasm. + +## Versioning + +This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read the specification [here](https://semver.org/spec/v2.0.0.html). +!*/ #![doc( html_logo_url = "https://raw.githubusercontent.com/polyphony-chat/design/main/branding/polyphony-chorus-round-8bit.png" )] @@ -14,11 +101,23 @@ clippy::new_without_default, clippy::useless_conversion )] +#![warn( + clippy::todo, + clippy::unimplemented, + clippy::dbg_macro, + clippy::print_stdout, + clippy::print_stderr +)] #[cfg(all(feature = "rt", feature = "rt_multi_thread"))] compile_error!("feature \"rt\" and feature \"rt_multi_thread\" cannot be enabled at the same time"); +use errors::ChorusResult; +use serde::{Deserialize, Serialize}; +use types::types::domains_configuration::WellKnownResponse; use url::{ParseError, Url}; +use crate::errors::ChorusError; + #[cfg(feature = "client")] pub mod api; pub mod errors; @@ -32,7 +131,7 @@ pub mod types; #[cfg(feature = "client")] pub mod voice; -#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] /// A URLBundle bundles together the API-, Gateway- and CDN-URLs of a Spacebar instance. /// /// # Notes @@ -80,7 +179,7 @@ impl UrlBundle { let url_fmt = format!("http://{}", url); return UrlBundle::parse_url(url_fmt); } - Err(_) => panic!("Invalid URL"), + Err(_) => panic!("Invalid URL"), // TODO: should not panic here }; // if the last character of the string is a slash, remove it. let mut url_string = url.to_string(); @@ -89,6 +188,63 @@ impl UrlBundle { } url_string } + + /// Performs a few HTTP requests to try and retrieve a `UrlBundle` from an instances' root url. + /// The method tries to retrieve the `UrlBundle` via these three strategies, in order: + /// - GET: `$url/.well-known/spacebar` -> Retrieve UrlBundle via `$wellknownurl/api/policies/instance/domains` + /// - GET: `$url/api/policies/instance/domains` + /// - GET: `$url/policies/instance/domains` + /// + /// The URL stored at `.well-known/spacebar` is the instances' API endpoint. The API + /// stores the CDN and WSS URLs under the `$api/policies/instance/domains` endpoint. If all three + /// of the above approaches fail, it is very likely that the instance is misconfigured, unreachable, or that + /// a wrong URL was provided. + pub async fn from_root_url(url: &str) -> ChorusResult { + let parsed = UrlBundle::parse_url(url.to_string()); + let client = reqwest::Client::new(); + let request_wellknown = client + .get(format!("{}/.well-known/spacebar", &parsed)) + .header(http::header::ACCEPT, "application/json") + .build()?; + let response_wellknown = client.execute(request_wellknown).await?; + if response_wellknown.status().is_success() { + let body = response_wellknown.json::().await?.api; + UrlBundle::from_api_url(&body).await + } else { + if let Ok(response_slash_api) = + UrlBundle::from_api_url(&format!("{}/api/policies/instance/domains", parsed)).await + { + return Ok(response_slash_api); + } + if let Ok(response_api) = + UrlBundle::from_api_url(&format!("{}/policies/instance/domains", parsed)).await + { + Ok(response_api) + } else { + Err(ChorusError::RequestFailed { url: parsed.to_string(), error: "Could not retrieve UrlBundle from url after trying 3 different approaches. Check the provided Url and make sure the instance is reachable.".to_string() } ) + } + } + } + + async fn from_api_url(url: &str) -> ChorusResult { + let client = reqwest::Client::new(); + let request = client + .get(url) + .header(http::header::ACCEPT, "application/json") + .build()?; + let response = client.execute(request).await?; + if let Ok(body) = response + .json::() + .await + { + Ok(UrlBundle::new(body.api_endpoint, body.gateway, body.cdn)) + } else { + Err(ChorusError::RequestFailed { + url: url.to_string(), + error: "Could not retrieve a UrlBundle from the given url. Check the provided url and make sure the instance is reachable.".to_string(), + }) + } + } } #[cfg(test)] diff --git a/src/ratelimiter.rs b/src/ratelimiter.rs index 88d4a02..f6a7c26 100644 --- a/src/ratelimiter.rs +++ b/src/ratelimiter.rs @@ -349,7 +349,7 @@ impl ChorusRequest { /// /// # Reference /// See - pub(crate) async fn get_limits_config(url_api: &str) -> ChorusResult { + pub async fn get_limits_config(url_api: &str) -> ChorusResult { let request = Client::new() .get(format!("{}/policies/instance/limits/", url_api)) .send() diff --git a/src/types/config/types/domains_configuration.rs b/src/types/config/types/domains_configuration.rs new file mode 100644 index 0000000..297b827 --- /dev/null +++ b/src/types/config/types/domains_configuration.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Clone, Debug)] +/// Represents the result of the `$rooturl/.well-known/spacebar` endpoint. +/// +/// See for more information. +pub struct WellKnownResponse { + pub api: String, +} + +#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Clone, Debug)] +#[serde(rename_all = "camelCase")] +/// Represents the result of the `$api/policies/instance/domains` endpoint. +pub struct Domains { + pub cdn: String, + pub gateway: String, + pub api_endpoint: String, + pub default_api_version: String, +} + +impl std::fmt::Display for Domains { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{\n\tCDN URL: {},\n\tGateway URL: {},\n\tAPI Endpoint: {},\n\tDefault API Version: {}\n}}", + self.cdn, self.gateway, self.api_endpoint, self.default_api_version + ) + } +} diff --git a/src/types/config/types/general_configuration.rs b/src/types/config/types/general_configuration.rs index 13b3aa8..450fd52 100644 --- a/src/types/config/types/general_configuration.rs +++ b/src/types/config/types/general_configuration.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::types::utils::Snowflake; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] pub struct GeneralConfiguration { pub instance_name: String, diff --git a/src/types/config/types/mod.rs b/src/types/config/types/mod.rs index dce4eb0..6ea2c03 100644 --- a/src/types/config/types/mod.rs +++ b/src/types/config/types/mod.rs @@ -1,6 +1,7 @@ pub mod api_configuration; pub mod cdn_configuration; pub mod defaults_configuration; +pub mod domains_configuration; pub mod email_configuration; pub mod endpoint_configuration; pub mod external_tokens_configuration; diff --git a/src/types/config/types/subconfigs/limits/ratelimits/auth.rs b/src/types/config/types/subconfigs/limits/ratelimits/auth.rs index f5abb0f..9815a5d 100644 --- a/src/types/config/types/subconfigs/limits/ratelimits/auth.rs +++ b/src/types/config/types/subconfigs/limits/ratelimits/auth.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::types::config::types::subconfigs::limits::ratelimits::RateLimitOptions; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct AuthRateLimit { pub login: RateLimitOptions, pub register: RateLimitOptions, diff --git a/src/types/config/types/subconfigs/limits/ratelimits/mod.rs b/src/types/config/types/subconfigs/limits/ratelimits/mod.rs index 66a2b78..934b922 100644 --- a/src/types/config/types/subconfigs/limits/ratelimits/mod.rs +++ b/src/types/config/types/subconfigs/limits/ratelimits/mod.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; pub mod auth; pub mod route; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] pub struct RateLimitOptions { pub bot: Option, diff --git a/src/types/config/types/subconfigs/limits/ratelimits/route.rs b/src/types/config/types/subconfigs/limits/ratelimits/route.rs index 5529af3..1aa0be2 100644 --- a/src/types/config/types/subconfigs/limits/ratelimits/route.rs +++ b/src/types/config/types/subconfigs/limits/ratelimits/route.rs @@ -4,7 +4,7 @@ use crate::types::config::types::subconfigs::limits::ratelimits::{ auth::AuthRateLimit, RateLimitOptions, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct RouteRateLimit { pub guild: RateLimitOptions, pub webhook: RateLimitOptions, diff --git a/src/types/config/types/subconfigs/limits/rates.rs b/src/types/config/types/subconfigs/limits/rates.rs index 8fdd183..642dcc8 100644 --- a/src/types/config/types/subconfigs/limits/rates.rs +++ b/src/types/config/types/subconfigs/limits/rates.rs @@ -7,7 +7,7 @@ use crate::types::{ LimitType, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct RateLimits { pub enabled: bool, pub ip: RateLimitOptions, diff --git a/src/types/entities/ratelimits.rs b/src/types/entities/ratelimits.rs index a95a2c6..f766a12 100644 --- a/src/types/entities/ratelimits.rs +++ b/src/types/entities/ratelimits.rs @@ -25,7 +25,7 @@ pub enum LimitType { /// A struct that represents the current ratelimits, either instance-wide or user-wide. /// See for more information. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct Limit { pub bucket: LimitType, pub limit: u64, diff --git a/tests/auth.rs b/tests/auth.rs index 086c8ba..130bfb6 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -1,5 +1,4 @@ -use chorus::types::RegisterSchema; -// PRETTYFYME: Move common wasm setup to common.rs +use chorus::types::{LoginSchema, RegisterSchema}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] @@ -10,13 +9,91 @@ mod common; #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn test_registration() { - let bundle = common::setup().await; + let mut bundle = common::setup().await; let reg = RegisterSchema { username: "Hiiii".into(), date_of_birth: Some("2000-01-01".to_string()), consent: true, ..Default::default() }; - bundle.instance.clone().register_account(reg).await.unwrap(); + bundle.instance.register_account(reg).await.unwrap(); + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_login() { + let mut bundle = common::setup().await; + let reg = RegisterSchema { + username: "Hiiii".into(), + email: Some("testuser1@integrationtesting.xyz".into()), + password: Some("Correct-Horse-Battery-Staple1".into()), + date_of_birth: Some("2000-01-01".to_string()), + consent: true, + ..Default::default() + }; + bundle.instance.register_account(reg).await.unwrap(); + let login = LoginSchema { + login: "testuser1@integrationtesting.xyz".into(), + password: "Correct-Horse-Battery-Staple1".into(), + ..Default::default() + }; + bundle.instance.login_account(login).await.unwrap(); + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_wrong_login() { + let mut bundle = common::setup().await; + let reg = RegisterSchema { + username: "Hiiii".into(), + email: Some("testuser2@integrationtesting.xyz".into()), + password: Some("Correct-Horse-Battery-Staple1".into()), + date_of_birth: Some("2000-01-01".to_string()), + consent: true, + ..Default::default() + }; + bundle.instance.register_account(reg).await.unwrap(); + let login = LoginSchema { + login: "testuser2@integrationtesting.xyz".into(), + password: "Correct-Horse-Battery-Staple2".into(), + ..Default::default() + }; + let res = bundle.instance.login_account(login).await; + assert!(res.is_err()); + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_login_with_token() { + let mut bundle = common::setup().await; + + let token = &bundle.user.token; + let other_user = bundle + .instance + .login_with_token(token.clone()) + .await + .unwrap(); + assert_eq!( + bundle.user.object.read().unwrap().id, + other_user.object.read().unwrap().id + ); + assert_eq!(bundle.user.token, other_user.token); + + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_login_with_invalid_token() { + let mut bundle = common::setup().await; + + let token = "invalid token lalalalala".to_string(); + let other_user = bundle.instance.login_with_token(token.clone()).await; + + assert!(other_user.is_err()); + common::teardown(bundle).await; } diff --git a/tests/channels.rs b/tests/channels.rs index 864165a..d9842c6 100644 --- a/tests/channels.rs +++ b/tests/channels.rs @@ -4,7 +4,6 @@ use chorus::types::{ }; mod common; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b533fd2..d6aaa34 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -57,7 +57,7 @@ pub(crate) async fn setup() -> TestBundle { "ws://localhost:3001".to_string(), "http://localhost:3001".to_string(), ); - let instance = Instance::new(urls.clone(), true).await.unwrap(); + let instance = Instance::new(urls.clone()).await.unwrap(); // Requires the existance of the below user. let reg = RegisterSchema { username: "integrationtestuser".into(), diff --git a/tests/gateway.rs b/tests/gateway.rs index 68a203a..5bf5865 100644 --- a/tests/gateway.rs +++ b/tests/gateway.rs @@ -5,7 +5,6 @@ use std::sync::{Arc, RwLock}; use chorus::errors::GatewayError; use chorus::gateway::*; use chorus::types::{self, ChannelModifySchema, RoleCreateModifySchema, RoleObject}; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/guilds.rs b/tests/guilds.rs index 360113d..ab955de 100644 --- a/tests/guilds.rs +++ b/tests/guilds.rs @@ -3,7 +3,6 @@ use chorus::types::{ }; mod common; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/instance.rs b/tests/instance.rs index 56f4d6d..f1243a5 100644 --- a/tests/instance.rs +++ b/tests/instance.rs @@ -1,5 +1,4 @@ mod common; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/invites.rs b/tests/invites.rs index d830ee8..ae1b9ab 100644 --- a/tests/invites.rs +++ b/tests/invites.rs @@ -1,6 +1,5 @@ mod common; use chorus::types::CreateChannelInviteSchema; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/members.rs b/tests/members.rs index c9072ef..9b415c3 100644 --- a/tests/members.rs +++ b/tests/members.rs @@ -1,5 +1,4 @@ use chorus::{errors::ChorusResult, types::GuildMember}; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/messages.rs b/tests/messages.rs index fc59a42..7ff7598 100644 --- a/tests/messages.rs +++ b/tests/messages.rs @@ -2,7 +2,6 @@ use std::fs::File; use std::io::{BufReader, Read}; use chorus::types::{self, Guild, Message, MessageSearchQuery}; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/relationships.rs b/tests/relationships.rs index 156f6eb..c8ee9cc 100644 --- a/tests/relationships.rs +++ b/tests/relationships.rs @@ -1,5 +1,4 @@ use chorus::types::{self, Relationship, RelationshipType}; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/roles.rs b/tests/roles.rs index ca58582..8dda704 100644 --- a/tests/roles.rs +++ b/tests/roles.rs @@ -1,5 +1,4 @@ use chorus::types::{self, RoleCreateModifySchema, RoleObject}; -// PRETTYFYME: Move common wasm setup to common.rs #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] diff --git a/tests/urlbundle.rs b/tests/urlbundle.rs new file mode 100644 index 0000000..790229b --- /dev/null +++ b/tests/urlbundle.rs @@ -0,0 +1,26 @@ +use chorus::types::types::domains_configuration::WellKnownResponse; +use chorus::UrlBundle; +use serde_json::json; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::*; +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_parse_url() { + // TODO: Currently only tests two of the three branches in UrlBundle::from_root_domain. + let url = url::Url::parse("http://localhost:3001/").unwrap(); + UrlBundle::from_root_url(url.as_str()).await.unwrap(); + let url = url::Url::parse("http://localhost:3001/api/").unwrap(); + UrlBundle::from_root_url(url.as_str()).await.unwrap(); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_parse_wellknown() { + let json = json!({ + "api": "http://localhost:3001/api/v9" + }); + let _well_known: WellKnownResponse = serde_json::from_value(json).unwrap(); +}