diff --git a/examples/login.rs b/examples/login.rs index e89d8d2..2449d6c 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -24,5 +24,5 @@ async fn main() { .await .expect("An error occurred during the login process"); dbg!(user.belongs_to); - dbg!(&user.object.read().unwrap().username); + dbg!(&user.object.unwrap().as_ref().read().unwrap().username); } diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 7c58e0e..e57474e 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -11,7 +11,9 @@ use crate::errors::ChorusResult; use crate::gateway::Gateway; use crate::instance::{ChorusUser, Instance}; use crate::ratelimiter::ChorusRequest; -use crate::types::{GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, User}; +use crate::types::{ + AuthenticatorType, GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, SendMfaSmsResponse, SendMfaSmsSchema, User, VerifyMFALoginResponse, VerifyMFALoginSchema +}; impl Instance { /// Logs into an existing account on the spacebar server. @@ -32,7 +34,7 @@ impl Instance { // instances' limits to pass them on as user_rate_limits later. let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; - + let login_result = chorus_request .deserialize_response::(&mut user) .await?; @@ -40,7 +42,7 @@ impl Instance { user.settings = login_result.settings; let object = User::get(&mut user, None).await?; - *user.object.write().unwrap() = object; + user.object = Some(Arc::new(RwLock::new(object))); let mut identify = GatewayIdentifyPayload::common(); identify.token = user.token(); @@ -48,4 +50,59 @@ impl Instance { Ok(user) } + + /// Verifies a multi-factor authentication login + /// + /// # Reference + /// See + pub async fn verify_mfa_login( + &mut self, + authenticator: AuthenticatorType, + schema: VerifyMFALoginSchema, + ) -> ChorusResult { + let endpoint_url = self.urls.api.clone() + &authenticator.to_string(); + + let chorus_request = ChorusRequest { + request: Client::new() + .post(endpoint_url) + .header("Content-Type", "application/json") + .json(&schema), + limit_type: LimitType::AuthLogin, + }; + + let mut user = + ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; + + match chorus_request.deserialize_response::(&mut user).await? { + VerifyMFALoginResponse::Success { token, user_settings: _ } => user.set_token(token), + VerifyMFALoginResponse::UserSuspended { suspended_user_token } => return Err(crate::errors::ChorusError::SuspendUser { token: suspended_user_token }), + } + + let mut identify = GatewayIdentifyPayload::common(); + identify.token = user.token(); + user.gateway.send_identify(identify).await; + + Ok(user) + } + + /// Sends a multi-factor authentication code to the user's phone number + /// + /// # Reference + /// See + pub async fn send_mfa_sms(&mut self, schema: SendMfaSmsSchema) -> ChorusResult { + let endpoint_url = self.urls.api.clone() + "/auth/mfa/sms/send"; + let chorus_request = ChorusRequest { + request: Client::new() + .post(endpoint_url) + .header("Content-Type", "application/json") + .json(&schema), + limit_type: LimitType::Ip + }; + + let mut chorus_user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; + + let send_mfa_sms_response = chorus_request.deserialize_response::(&mut chorus_user).await?; + + Ok(send_mfa_sms_response) + } } diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index 498080e..e8a859c 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -29,7 +29,7 @@ impl Instance { let object = User::get(&mut user, None).await?; let settings = User::get_settings(&mut user).await?; - *user.object.write().unwrap() = object; + user.object = Some(Arc::new(RwLock::new(object))); *user.settings.write().unwrap() = settings; let mut identify = GatewayIdentifyPayload::common(); diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index 6b94a4d..5582b0b 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -49,7 +49,7 @@ impl Instance { let object = User::get(&mut user, None).await?; let settings = User::get_settings(&mut user).await?; - *user.object.write().unwrap() = object; + user.object = Some(Arc::new(RwLock::new(object))); *user.settings.write().unwrap() = settings; let mut identify = GatewayIdentifyPayload::common(); diff --git a/src/api/channels/channels.rs b/src/api/channels/channels.rs index 6c41576..7cb9ee3 100644 --- a/src/api/channels/channels.rs +++ b/src/api/channels/channels.rs @@ -30,7 +30,6 @@ impl Channel { ), None, None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -61,7 +60,6 @@ impl Channel { &url, None, audit_log_reason.as_deref(), - None, Some(user), LimitType::Channel(self.id), ); @@ -101,7 +99,6 @@ impl Channel { &url, Some(to_string(&modify_data).unwrap()), audit_log_reason.as_deref(), - None, Some(user), LimitType::Channel(channel_id), ); @@ -134,7 +131,6 @@ impl Channel { &url, None, None, - None, Some(user), Default::default(), ); @@ -196,7 +192,6 @@ impl Channel { &url, None, None, - None, Some(user), LimitType::Channel(self.id), ); @@ -225,7 +220,6 @@ impl Channel { &url, Some(to_string(&schema).unwrap()), None, - None, Some(user), LimitType::Guild(guild_id), ); diff --git a/src/api/channels/messages.rs b/src/api/channels/messages.rs index feabc37..af990c6 100644 --- a/src/api/channels/messages.rs +++ b/src/api/channels/messages.rs @@ -151,7 +151,6 @@ impl Message { .as_str(), None, None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -182,7 +181,6 @@ impl Message { .as_str(), None, audit_log_reason, - None, Some(user), LimitType::Channel(channel_id), ); @@ -209,7 +207,6 @@ impl Message { .as_str(), None, audit_log_reason, - None, Some(user), LimitType::Channel(channel_id), ); @@ -258,7 +255,6 @@ impl Message { .as_str(), Some(to_string(&schema).unwrap()), None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -292,7 +288,6 @@ impl Message { .as_str(), Some(to_string(&schema).unwrap()), None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -321,7 +316,6 @@ impl Message { .as_str(), None, None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -348,7 +342,6 @@ impl Message { &url, None, None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -382,7 +375,6 @@ impl Message { &url, Some(to_string(&schema).unwrap()), None, - None, Some(user), LimitType::Channel(channel_id), ); @@ -409,7 +401,6 @@ impl Message { &url, None, audit_log_reason.as_deref(), - None, Some(user), LimitType::Channel(channel_id), ); @@ -447,7 +438,6 @@ impl Message { .as_str(), Some(to_string(&messages).unwrap()), audit_log_reason.as_deref(), - None, Some(user), LimitType::Channel(channel_id), ); @@ -472,7 +462,6 @@ impl Message { .as_str(), None, None, - None, Some(user), LimitType::Channel(channel_id), ); diff --git a/src/api/channels/permissions.rs b/src/api/channels/permissions.rs index 03465b8..19492cd 100644 --- a/src/api/channels/permissions.rs +++ b/src/api/channels/permissions.rs @@ -83,7 +83,6 @@ impl types::Channel { &url, None, None, - None, Some(user), LimitType::Channel(channel_id), ); diff --git a/src/api/channels/reactions.rs b/src/api/channels/reactions.rs index f2de33d..e9bec80 100644 --- a/src/api/channels/reactions.rs +++ b/src/api/channels/reactions.rs @@ -36,7 +36,6 @@ impl ReactionMeta { &url, None, None, - None, Some(user), LimitType::Channel(self.channel_id), ); @@ -65,7 +64,6 @@ impl ReactionMeta { &url, None, None, - None, Some(user), LimitType::Channel(self.channel_id), ); @@ -96,7 +94,6 @@ impl ReactionMeta { &url, None, None, - None, Some(user), LimitType::Channel(self.channel_id), ); @@ -130,7 +127,6 @@ impl ReactionMeta { &url, None, None, - None, Some(user), LimitType::Channel(self.channel_id), ); @@ -159,7 +155,6 @@ impl ReactionMeta { &url, None, None, - None, Some(user), LimitType::Channel(self.channel_id), ); @@ -196,7 +191,6 @@ impl ReactionMeta { &url, None, None, - None, Some(user), LimitType::Channel(self.channel_id), ); diff --git a/src/api/guilds/guilds.rs b/src/api/guilds/guilds.rs index e2ff9ba..615600b 100644 --- a/src/api/guilds/guilds.rs +++ b/src/api/guilds/guilds.rs @@ -220,7 +220,6 @@ impl Guild { .as_str(), None, None, - None, Some(user), LimitType::Guild(guild_id), ); @@ -246,7 +245,6 @@ impl Guild { .as_str(), None, None, - None, Some(user), LimitType::Guild(guild_id), ); @@ -279,7 +277,6 @@ impl Guild { .as_str(), None, audit_log_reason.as_deref(), - None, Some(user), LimitType::Guild(guild_id), ); @@ -309,7 +306,6 @@ impl Guild { .as_str(), Some(to_string(&schema).unwrap()), audit_log_reason.as_deref(), - None, Some(user), LimitType::Guild(guild_id), ); @@ -336,7 +332,6 @@ impl Guild { .as_str(), Some(to_string(&schema).unwrap()), audit_log_reason.as_deref(), - None, Some(user), LimitType::Guild(guild_id), ); @@ -362,7 +357,6 @@ impl Guild { .as_str(), Some(to_string(&schema).unwrap()), None, - None, Some(user), LimitType::Guild(guild_id), ); @@ -393,7 +387,6 @@ impl Guild { &url, None, None, - None, Some(user), LimitType::Guild(guild_id), ); @@ -426,7 +419,6 @@ impl Guild { &url, None, None, - None, Some(user), LimitType::Guild(guild_id), ); @@ -456,7 +448,6 @@ impl Guild { .as_str(), Some(to_string(&schema).unwrap()), audit_log_reason.as_deref(), - None, Some(user), LimitType::Guild(guild_id), ); @@ -487,7 +478,6 @@ impl Guild { &url, None, audit_log_reason.as_deref(), - None, Some(user), LimitType::Guild(guild_id), ); diff --git a/src/api/guilds/roles.rs b/src/api/guilds/roles.rs index 6100a48..7e76010 100644 --- a/src/api/guilds/roles.rs +++ b/src/api/guilds/roles.rs @@ -188,7 +188,6 @@ impl types::RoleObject { &url, None, audit_log_reason.as_deref(), - None, Some(user), LimitType::Guild(guild_id), ); diff --git a/src/errors.rs b/src/errors.rs index 0d130dd..09bf7a1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,7 +5,7 @@ //! Contains all the errors that can be returned by the library. use custom_error::custom_error; -use crate::types::WebSocketEvent; +use crate::types::{MfaRequiredSchema, WebSocketEvent}; use chorus_macros::WebSocketEvent; custom_error! { @@ -46,7 +46,11 @@ custom_error! { /// Malformed or unexpected response. InvalidResponse{error: String} = "The response is malformed and cannot be processed. Error: {error}", /// Invalid, insufficient or too many arguments provided. - InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}" + InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}", + /// The request requires MFA verification + MfaRequired {error: MfaRequiredSchema} = "Mfa verification is required to perform this action", + /// The user's account is suspended + SuspendUser { token: String } = "Your account has been suspended" } impl From for ChorusError { @@ -151,4 +155,3 @@ custom_error! { CannotBind{error: String} = "Cannot bind socket due to a UDP error: {error}", CannotConnect{error: String} = "Cannot connect due to a UDP error: {error}", } - diff --git a/src/instance.rs b/src/instance.rs index f826ab5..6a5e8de 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -8,16 +8,18 @@ use std::collections::HashMap; use std::fmt; use std::sync::{Arc, RwLock}; +use std::time::Duration; use reqwest::Client; use serde::{Deserialize, Serialize}; +use chrono::Utc; use crate::errors::ChorusResult; use crate::gateway::{Gateway, GatewayHandle, GatewayOptions}; use crate::ratelimiter::ChorusRequest; use crate::types::types::subconfigs::limits::rates::RateLimits; use crate::types::{ - GeneralConfiguration, Limit, LimitType, LimitsConfiguration, Shared, User, UserSettings, + GeneralConfiguration, Limit, LimitType, LimitsConfiguration, MfaTokenSchema, MfaVerifySchema, Shared, User, UserSettings, MfaToken }; use crate::UrlBundle; @@ -152,9 +154,10 @@ impl fmt::Display for Token { pub struct ChorusUser { pub belongs_to: Shared, pub token: String, + pub mfa_token: Option, pub limits: Option>, pub settings: Shared, - pub object: Shared, + pub object: Option>, pub gateway: GatewayHandle, } @@ -177,12 +180,13 @@ impl ChorusUser { token: String, limits: Option>, settings: Shared, - object: Shared, + object: Option>, gateway: GatewayHandle, ) -> ChorusUser { ChorusUser { belongs_to, token, + mfa_token: None, limits, settings, object, @@ -197,7 +201,6 @@ impl ChorusUser { /// first. pub(crate) async fn shell(instance: Shared, token: String) -> ChorusUser { let settings = Arc::new(RwLock::new(UserSettings::default())); - let object = Arc::new(RwLock::new(User::default())); let wss_url = instance.read().unwrap().urls.wss.clone(); // Dummy gateway object let gateway = Gateway::spawn(wss_url, GatewayOptions::default()) @@ -205,6 +208,7 @@ impl ChorusUser { .unwrap(); ChorusUser { token, + mfa_token: None, belongs_to: instance.clone(), limits: instance .read() @@ -213,8 +217,41 @@ impl ChorusUser { .as_ref() .map(|info| info.ratelimits.clone()), settings, - object, + object: None, gateway, } } + + /// Sends a request to complete an MFA challenge. + /// + /// If successful, the MFA verification JWT returned is set on the current [ChorusUser] executing the + /// request. + /// + /// The JWT token expires after 5 minutes. + /// + /// # Reference + /// See + pub async fn complete_mfa_challenge(&mut self, mfa_verify_schema: MfaVerifySchema) -> ChorusResult<()> { + let endpoint_url = self.belongs_to.read().unwrap().urls.api.clone() + "/mfa/finish"; + let chorus_request = ChorusRequest { + request: Client::new() + .post(endpoint_url) + .header("Authorization", self.token()) + .json(&mfa_verify_schema), + limit_type: match self.object.is_some() { + true => LimitType::Global, + false => LimitType::Ip, + }, + }; + + let mfa_token_schema = chorus_request + .deserialize_response::(self).await?; + + self.mfa_token = Some(MfaToken { + token: mfa_token_schema.token, + expires_at: Utc::now() + Duration::from_secs(60 * 5), + }); + + Ok(()) + } } diff --git a/src/ratelimiter.rs b/src/ratelimiter.rs index 5ffcca6..785c57d 100644 --- a/src/ratelimiter.rs +++ b/src/ratelimiter.rs @@ -13,7 +13,7 @@ use serde_json::from_str; use crate::{ errors::{ChorusError, ChorusResult}, instance::ChorusUser, - types::{types::subconfigs::limits::rates::RateLimits, Limit, LimitType, LimitsConfiguration}, + types::{types::subconfigs::limits::rates::RateLimits, Limit, LimitType, LimitsConfiguration, MfaRequiredSchema}, }; /// Chorus' request struct. This struct is used to send rate-limited requests to the Spacebar server. @@ -34,13 +34,12 @@ impl ChorusRequest { /// * [`http::Method::DELETE`] /// * [`http::Method::PATCH`] /// * [`http::Method::HEAD`] - #[allow(unused_variables)] // TODO: Add mfa_token to request, once we figure out *how* to do so correctly + #[allow(unused_variables)] pub fn new( method: http::Method, url: &str, body: Option, audit_log_reason: Option<&str>, - mfa_token: Option<&str>, chorus_user: Option<&mut ChorusUser>, limit_type: LimitType, ) -> ChorusRequest { @@ -266,7 +265,14 @@ impl ChorusRequest { async fn interpret_error(response: reqwest::Response) -> ChorusError { match response.status().as_u16() { - 401..=403 | 407 => ChorusError::NoPermission, + 401 => { + let response = response.text().await.unwrap(); + match serde_json::from_str::(&response) { + Ok(response) => ChorusError::MfaRequired { error: response }, + Err(_) => ChorusError::NoPermission, + } + } + 402..=403 | 407 => ChorusError::NoPermission, 404 => ChorusError::NotFound { error: response.text().await.unwrap(), }, diff --git a/src/types/entities/mfa_token.rs b/src/types/entities/mfa_token.rs new file mode 100644 index 0000000..3bd5c13 --- /dev/null +++ b/src/types/entities/mfa_token.rs @@ -0,0 +1,11 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone)] +pub struct MfaToken { + pub token: String, + pub expires_at: DateTime, +} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index 2fec4ca..e1a3027 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -26,6 +26,7 @@ pub use user::*; pub use user_settings::*; pub use voice_state::*; pub use webhook::*; +pub use mfa_token::*; use crate::types::Shared; #[cfg(feature = "client")] @@ -67,6 +68,7 @@ mod user; mod user_settings; mod voice_state; mod webhook; +mod mfa_token; #[cfg(feature = "client")] #[async_trait(?Send)] diff --git a/src/types/schema/auth.rs b/src/types/schema/auth.rs index 83c88dc..77f1095 100644 --- a/src/types/schema/auth.rs +++ b/src/types/schema/auth.rs @@ -36,11 +36,25 @@ pub struct LoginSchema { pub gift_code_sku_id: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub struct TotpSchema { - code: String, - ticket: String, - gift_code_sku_id: Option, - login_source: Option, +pub struct VerifyMFALoginSchema { + pub ticket: String, + pub code: String, + pub login_source: Option, + pub gift_code_sku_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum VerifyMFALoginResponse { + Success { token: String, user_settings: LoginSettings }, + UserSuspended { suspended_user_token: String } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct LoginSettings { + pub locale: String, + pub theme: String, } diff --git a/src/types/schema/mfa.rs b/src/types/schema/mfa.rs new file mode 100644 index 0000000..f3b1e8b --- /dev/null +++ b/src/types/schema/mfa.rs @@ -0,0 +1,88 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub struct MfaRequiredSchema { + pub message: String, + pub code: i32, + pub mfa: MfaVerificationSchema, +} + +impl Display for MfaRequiredSchema { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MfaRequired") + .field("message", &self.message) + .field("code", &self.code) + .field("mfa", &self.mfa) + .finish() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub struct MfaVerificationSchema { + pub ticket: String, + pub methods: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub struct MfaMethod { + #[serde(rename = "type")] + pub kind: AuthenticatorType, + #[serde(skip_serializing_if = "Option::is_none")] + pub challenge: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub backup_codes_allowed: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum AuthenticatorType { + TOTP, + SMS, + Backup, + WebAuthn, + Password, +} + +impl Display for AuthenticatorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + AuthenticatorType::TOTP => "totp", + AuthenticatorType::SMS => "sms", + AuthenticatorType::Backup => "backup", + AuthenticatorType::WebAuthn => "webauthn", + AuthenticatorType::Password => "password", + } + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct MfaVerifySchema { + pub ticket: String, + pub mfa_type: AuthenticatorType, + pub data: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MfaTokenSchema { + pub token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SendMfaSmsSchema { + pub ticket: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SendMfaSmsResponse { + pub phone: String, +} diff --git a/src/types/schema/mod.rs b/src/types/schema/mod.rs index 09e542e..0133d57 100644 --- a/src/types/schema/mod.rs +++ b/src/types/schema/mod.rs @@ -5,6 +5,7 @@ pub use apierror::*; pub use audit_log::*; pub use auth::*; +pub use mfa::*; pub use channel::*; pub use guild::*; pub use message::*; @@ -17,6 +18,7 @@ pub use voice_state::*; mod apierror; mod audit_log; mod auth; +mod mfa; mod channel; mod guild; mod message; diff --git a/tests/auth.rs b/tests/auth.rs index 705328a..10f7195 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -2,9 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use std::str::FromStr; +use std::{os::unix::fs::chroot, str::FromStr}; -use chorus::types::{LoginSchema, RegisterSchema}; +use chorus::{instance::ChorusUser, types::{AuthenticatorType, LoginSchema, MfaVerifySchema, RegisterSchema, SendMfaSmsSchema}}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] @@ -85,8 +85,8 @@ async fn test_login_with_token() { .await .unwrap(); assert_eq!( - bundle.user.object.read().unwrap().id, - other_user.object.read().unwrap().id + bundle.user.object.as_ref().unwrap().read().unwrap().id, + other_user.object.unwrap().read().unwrap().id ); assert_eq!(bundle.user.token, other_user.token); @@ -105,3 +105,120 @@ async fn test_login_with_invalid_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_complete_mfa_challenge_totp() { + let mut bundle = common::setup().await; + + let token = "".to_string(); + let mut chorus_user = bundle.instance.login_with_token(token).await + .unwrap(); + + let schema = MfaVerifySchema { + ticket: "".to_string(), + mfa_type: AuthenticatorType::TOTP, + data: "".to_string(), + }; + + let result = chorus_user.complete_mfa_challenge(schema) + .await; + + assert!(result.is_ok()) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_complete_mfa_challenge_sms() { + let mut bundle = common::setup().await; + + let token = "".to_string(); + let mut chorus_user = bundle.instance.login_with_token(token).await + .unwrap(); + + let schema = MfaVerifySchema { + ticket: "".to_string(), + mfa_type: AuthenticatorType::SMS, + data: "".to_string(), + }; + + let result = chorus_user.complete_mfa_challenge(schema) + .await; + + assert!(result.is_ok()) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_verify_mfa_login_webauthn() { + let mut bundle = common::setup().await; + + let token = "".to_string(); + let mut chorus_user = bundle.instance.login_with_token(token).await + .unwrap(); + + let schema = MfaVerifySchema { + ticket: "".to_string(), + mfa_type: AuthenticatorType::SMS, + data: "".to_string(), + }; + + let result = chorus_user.complete_mfa_challenge(schema) + .await; + + assert!(result.is_ok()) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_complete_mfa_challenge_backup() { + let mut bundle = common::setup().await; + + let token = "".to_string(); + let mut chorus_user = bundle.instance.login_with_token(token).await + .unwrap(); + + let schema = MfaVerifySchema { + ticket: "".to_string(), + mfa_type: AuthenticatorType::Backup, + data: "".to_string(), + }; + + let result = chorus_user.complete_mfa_challenge(schema) + .await; + + assert!(result.is_ok()) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_complete_mfa_challenge_password() { + let mut bundle = common::setup().await; + + let token = "".to_string(); + let mut chorus_user = bundle.instance.login_with_token(token).await + .unwrap(); + + let schema = MfaVerifySchema { + ticket: "".to_string(), + mfa_type: AuthenticatorType::Password, + data: "".to_string(), + }; + + let result = chorus_user.complete_mfa_challenge(schema) + .await; + + assert!(result.is_ok()) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_send_mfa_sms() { + let mut bundle = common::setup().await; + + let schema = SendMfaSmsSchema { ticket: "".to_string() }; + + let result = bundle.instance.send_mfa_sms(schema).await; + + assert!(result.is_ok()) +} diff --git a/tests/channels.rs b/tests/channels.rs index eb1c120..4269157 100644 --- a/tests/channels.rs +++ b/tests/channels.rs @@ -2,7 +2,11 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use chorus::types::{self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags, PermissionOverwrite, PermissionOverwriteType, PrivateChannelCreateSchema, RelationshipType, Snowflake}; +use chorus::types::{ + self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags, + PermissionOverwrite, PermissionOverwriteType, PrivateChannelCreateSchema, RelationshipType, + Snowflake, +}; mod common; @@ -67,7 +71,7 @@ async fn modify_channel() { assert_eq!(modified_channel.name, Some(CHANNEL_NAME.to_string())); let permission_override = PermissionFlags::MANAGE_CHANNELS | PermissionFlags::MANAGE_MESSAGES; - let user_id: types::Snowflake = bundle.user.object.read().unwrap().id; + let user_id: types::Snowflake = bundle.user.object.as_ref().unwrap().read().unwrap().id; let permission_override = PermissionOverwrite { id: user_id, overwrite_type: PermissionOverwriteType::Member, @@ -155,7 +159,13 @@ async fn create_dm() { let other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; let private_channel_create_schema = PrivateChannelCreateSchema { - recipients: Some(Vec::from([other_user.object.read().unwrap().id])), + recipients: Some(Vec::from([other_user + .object + .as_ref() + .unwrap() + .read() + .unwrap() + .id])), access_tokens: None, nicks: None, }; @@ -174,7 +184,7 @@ async fn create_dm() { .unwrap() .id .clone(), - other_user.object.read().unwrap().id + other_user.object.unwrap().read().unwrap().id ); assert_eq!( dm_channel @@ -187,7 +197,7 @@ async fn create_dm() { .unwrap() .id .clone(), - user.object.read().unwrap().id.clone() + user.object.as_ref().unwrap().read().unwrap().id.clone() ); common::teardown(bundle).await; } @@ -199,9 +209,9 @@ async fn remove_add_person_from_to_dm() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let mut third_user = bundle.create_user("integrationtestuser3").await; - let third_user_id = third_user.object.read().unwrap().id; - let other_user_id = other_user.object.read().unwrap().id; - let user_id = bundle.user.object.read().unwrap().id; + let third_user_id = third_user.object.as_ref().unwrap().read().unwrap().id; + let other_user_id = other_user.object.as_ref().unwrap().read().unwrap().id; + let user_id = bundle.user.object.as_ref().unwrap().read().unwrap().id; let user = &mut bundle.user; let private_channel_create_schema = PrivateChannelCreateSchema { recipients: Some(Vec::from([other_user_id, third_user_id])), diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f2f0663..4b7c053 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -47,6 +47,7 @@ impl TestBundle { ChorusUser { 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(), diff --git a/tests/guilds.rs b/tests/guilds.rs index 8399567..9eb5adc 100644 --- a/tests/guilds.rs +++ b/tests/guilds.rs @@ -60,7 +60,8 @@ async fn guild_create_ban() { .await .unwrap(); other_user.accept_invite(&invite.code, None).await.unwrap(); - let other_user_id = other_user.object.read().unwrap().id; + let other_user_id = other_user.object.as_ref().unwrap() + .read().unwrap().id; Guild::create_ban( guild.id, other_user_id, @@ -103,7 +104,9 @@ async fn guild_remove_member() { .await .unwrap(); other_user.accept_invite(&invite.code, None).await.unwrap(); - let other_user_id = other_user.object.read().unwrap().id; + let other_user_id = other_user.object + .as_ref().unwrap() + .read().unwrap().id; Guild::remove_member(guild.id, other_user_id, None, &mut bundle.user) .await .unwrap(); diff --git a/tests/members.rs b/tests/members.rs index a66d25a..0fc45d4 100644 --- a/tests/members.rs +++ b/tests/members.rs @@ -16,7 +16,8 @@ async fn add_remove_role() -> ChorusResult<()> { let mut bundle = common::setup().await; let guild = bundle.guild.read().unwrap().id; let role = bundle.role.read().unwrap().id; - let member_id = bundle.user.object.read().unwrap().id; + let member_id = bundle.user.object.as_ref().unwrap() + .read().unwrap().id; GuildMember::add_role(&mut bundle.user, guild, member_id, role).await?; let member = GuildMember::get(&mut bundle.user, guild, member_id) .await diff --git a/tests/messages.rs b/tests/messages.rs index c4ceca5..ee35d61 100644 --- a/tests/messages.rs +++ b/tests/messages.rs @@ -106,7 +106,7 @@ async fn search_messages() { let _arg = Some(&vec_attach); let message = bundle.user.send_message(message, channel.id).await.unwrap(); let query = MessageSearchQuery { - author_id: Some(Vec::from([bundle.user.object.read().unwrap().id])), + author_id: Some(Vec::from([bundle.user.object.as_ref().unwrap().read().unwrap().id])), ..Default::default() }; let guild_id = bundle.guild.read().unwrap().id; diff --git a/tests/relationships.rs b/tests/relationships.rs index 4e57b22..a9d2282 100644 --- a/tests/relationships.rs +++ b/tests/relationships.rs @@ -16,9 +16,24 @@ async fn test_get_mutual_relationships() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let username = user.object.read().unwrap().username.clone(); - let discriminator = user.object.read().unwrap().discriminator.clone(); - let other_user_id: types::Snowflake = other_user.object.read().unwrap().id; + + let username = user + .object + .as_ref() + .unwrap() + .read() + .unwrap() + .username + .clone(); + let discriminator = user + .object + .as_ref() + .unwrap() + .read() + .unwrap() + .discriminator + .clone(); + let other_user_id: types::Snowflake = other_user.object.as_ref().unwrap().read().unwrap().id; let friend_request_schema = types::FriendRequestSendSchema { username, discriminator: Some(discriminator), @@ -38,8 +53,22 @@ async fn test_get_relationships() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let username = user.object.read().unwrap().username.clone(); - let discriminator = user.object.read().unwrap().discriminator.clone(); + let username = user + .object + .as_ref() + .unwrap() + .read() + .unwrap() + .username + .clone(); + let discriminator = user + .object + .as_ref() + .unwrap() + .read() + .unwrap() + .discriminator + .clone(); let friend_request_schema = types::FriendRequestSendSchema { username, discriminator: Some(discriminator), @@ -51,7 +80,7 @@ async fn test_get_relationships() { let relationships = user.get_relationships().await.unwrap(); assert_eq!( relationships.first().unwrap().id, - other_user.object.read().unwrap().id + other_user.object.unwrap().read().unwrap().id ); common::teardown(bundle).await } @@ -62,8 +91,8 @@ async fn test_modify_relationship_friends() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let user_id: types::Snowflake = user.object.read().unwrap().id; - let other_user_id: types::Snowflake = other_user.object.read().unwrap().id; + let user_id: types::Snowflake = user.object.as_ref().unwrap().read().unwrap().id; + let other_user_id: types::Snowflake = other_user.object.as_ref().unwrap().read().unwrap().id; other_user .modify_user_relationship(user_id, types::RelationshipType::Friends) @@ -72,7 +101,7 @@ async fn test_modify_relationship_friends() { let relationships = user.get_relationships().await.unwrap(); assert_eq!( relationships.first().unwrap().id, - other_user.object.read().unwrap().id + other_user.object.as_ref().unwrap().read().unwrap().id ); assert_eq!( relationships.first().unwrap().relationship_type, @@ -81,7 +110,7 @@ async fn test_modify_relationship_friends() { let relationships = other_user.get_relationships().await.unwrap(); assert_eq!( relationships.first().unwrap().id, - user.object.read().unwrap().id + user.object.as_ref().unwrap().read().unwrap().id ); assert_eq!( relationships.first().unwrap().relationship_type, @@ -114,7 +143,7 @@ async fn test_modify_relationship_block() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let user_id: types::Snowflake = user.object.read().unwrap().id; + let user_id: types::Snowflake = user.object.as_ref().unwrap().read().unwrap().id; other_user .modify_user_relationship(user_id, types::RelationshipType::Blocked) @@ -125,7 +154,7 @@ async fn test_modify_relationship_block() { let relationships = other_user.get_relationships().await.unwrap(); assert_eq!( relationships.first().unwrap().id, - user.object.read().unwrap().id + user.object.as_ref().unwrap().read().unwrap().id ); assert_eq!( relationships.first().unwrap().relationship_type,