From 8025a910e72ec05c3e00c841128a2a52c09d317f Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:48:25 +0200 Subject: [PATCH 1/7] Initial pass of adding integration tests. This version just does the bare minimum and logs in and out again. --- Cargo.toml | 12 ++++++- integration/main.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 integration/main.rs diff --git a/Cargo.toml b/Cargo.toml index 7cc6aa6..ed6b417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,14 @@ uuid = { version = "^1.8", features = ["serde", "v4"] } reqwest = { version = "^0.12", features = ["json", "cookies", "multipart"] } [dev-dependencies] -tokio = { version = '1', features = ['macros', 'rt-multi-thread'] } \ No newline at end of file +tokio = { version = '1', features = ['macros', 'rt-multi-thread'] } +totp_rfc6238 = "0.6.1" +anyhow = "1" +thiserror = "1" +dotenv = "0.15" +base32 = "0.5.1" + +[[test]] +name = "integration" +path = "integration/main.rs" +harness = false \ No newline at end of file diff --git a/integration/main.rs b/integration/main.rs new file mode 100644 index 0000000..fe20ab5 --- /dev/null +++ b/integration/main.rs @@ -0,0 +1,79 @@ +use base32::Alphabet; +use vrchatapi::models::{EitherUserOrTwoFactor, TwoFactorAuthCode}; + +#[derive(Debug, thiserror::Error)] +enum Error{ + #[error("Dotenv failed to initialize: {0}")] + DotenvInitError(dotenv::Error), + #[error("No Username provided or error decoding username: {0}")] + NoUsername(dotenv::Error), + #[error("No Password provided or error decoding password: {0}")] + NoPassword(dotenv::Error), + #[error("No Totp Secret provided or error decoding totp secret: {0}")] + NoTotpSecret(dotenv::Error), + #[error("We were unable to login with the provided credentials.")] + UnableToLogin, + #[error("No totp 2fa variant available.")] + NoTotp2FAAvailable, + #[error("Failed to get the current user: {0}")] + GetCurrentUser(#[from] vrchatapi::apis::Error), + #[error("Failed to verify with 2fa: {0}")] + Verify2FA(#[from] vrchatapi::apis::Error), + #[error("Failed to logout: {0}")] + Logout(#[from] vrchatapi::apis::Error), + #[error("Failed to decode Totp from base32")] + TOTPBase32, +} + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +#[tokio::main] +async fn main() { + println!("Hello"); + match test().await { + Ok(()) => {} + Err(e) => eprint!("{e}") + } +} +async fn test() -> Result<(), anyhow::Error>{ + dotenv::dotenv().map_err(|e|Error::DotenvInitError(e))?; + let username = dotenv::var("USERNAME").map_err(|e|Error::NoUsername(e))?; + let password = dotenv::var("PASSWORD").map_err(|e|Error::NoPassword(e))?; + let totp_secret = dotenv::var("TOTP_SECRET").map_err(|e|Error::NoTotpSecret(e))?; + let totp_secret = base32::decode(Alphabet::Rfc4648Lower {padding: false}, totp_secret.as_str()).ok_or(Error::TOTPBase32)?; + let generator = totp_rfc6238::TotpGenerator::new() + .build(); + + let mut client = vrchatapi::apis::configuration::Configuration::default(); + client.user_agent = Some(format!("vrchatapi-rust@{VERSION} https://github.com/vrchatapi/vrchatapi-rust/issues/new")); + client.basic_auth = Some((username.clone(), Some(password))); + let u = match vrchatapi::apis::authentication_api::get_current_user(&client).await.map_err(|e|Error::GetCurrentUser(e))? { + EitherUserOrTwoFactor::CurrentUser(u) => u, + EitherUserOrTwoFactor::RequiresTwoFactorAuth(r2fa) => { + if !r2fa.requires_two_factor_auth.contains(&"totp".to_string()) { + let _ = logout(&client).await; //Ignore logout error. This is just here, to hopefully avoid session spam. + return Err(Error::NoTotp2FAAvailable)?; + } + + let code = generator.get_code(totp_secret.as_slice()); + println!("Generated code: {code}"); + if !vrchatapi::apis::authentication_api::verify2_fa(&client, TwoFactorAuthCode::new(code)).await.map_err(|e|Error::Verify2FA(e))?.verified { + let _ = logout(&client).await; //Ignore logout error. This is just here, to hopefully avoid session spam. + return Err(Error::UnableToLogin)?; + } + + match vrchatapi::apis::authentication_api::get_current_user(&client).await.map_err(|e|Error::GetCurrentUser(e))? { + EitherUserOrTwoFactor::CurrentUser(u) => u, + EitherUserOrTwoFactor::RequiresTwoFactorAuth(_) => return Err(Error::UnableToLogin)?, + } + } + }; + println!("Logged in as: {} (Login Name was {username}", u.username.as_ref().map(String::as_str).unwrap_or("Unknown User")); + + logout(&client).await?; + Ok(()) +} + +async fn logout(client: &vrchatapi::apis::configuration::Configuration) -> Result<(), anyhow::Error> { + vrchatapi::apis::authentication_api::logout(&client).await.map_err(|e|Error::Logout(e))?; + Ok(()) +} \ No newline at end of file From 02e156bb722544d9a7dcc1148441b195338c7c48 Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:50:17 +0200 Subject: [PATCH 2/7] Make Cargo.toml changes persistent --- generate.sh | 2 +- patches/Cargo.toml | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 patches/Cargo.toml diff --git a/generate.sh b/generate.sh index b11be0f..d805b38 100755 --- a/generate.sh +++ b/generate.sh @@ -28,7 +28,7 @@ sed -i 's/Client::new()/Client::builder().cookie_store(true).build().unwrap()/g' sed -i 's/features = \["json", "multipart"\]/features = \["json", "cookies", "multipart"\]/g' Cargo.toml #Fix example -printf "\n[dev-dependencies]\ntokio = { version = '1', features = ['macros', 'rt-multi-thread'] }" >> Cargo.toml +cat patches/Cargo.toml >> Cargo.toml # https://github.com/vrchatapi/specification/issues/241 cat patches/2FA_Current_User.rs >> src/models/current_user.rs diff --git a/patches/Cargo.toml b/patches/Cargo.toml new file mode 100644 index 0000000..bcf6157 --- /dev/null +++ b/patches/Cargo.toml @@ -0,0 +1,12 @@ +[dev-dependencies] +tokio = { version = '1', features = ['macros', 'rt-multi-thread'] } +totp_rfc6238 = "0.6.1" +anyhow = "1" +thiserror = "1" +dotenv = "0.15" +base32 = "0.5.1" + +[[test]] +name = "integration" +path = "integration/main.rs" +harness = false \ No newline at end of file From f21e9588bc5873bf613d3826173bb3b05319388d Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:07:36 +0200 Subject: [PATCH 3/7] Extract login into seperate method --- integration/main.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/integration/main.rs b/integration/main.rs index fe20ab5..40ce479 100644 --- a/integration/main.rs +++ b/integration/main.rs @@ -38,14 +38,22 @@ async fn test() -> Result<(), anyhow::Error>{ dotenv::dotenv().map_err(|e|Error::DotenvInitError(e))?; let username = dotenv::var("USERNAME").map_err(|e|Error::NoUsername(e))?; let password = dotenv::var("PASSWORD").map_err(|e|Error::NoPassword(e))?; + + let mut client = vrchatapi::apis::configuration::Configuration::default(); + client.user_agent = Some(format!("vrchatapi-rust@{VERSION} https://github.com/vrchatapi/vrchatapi-rust/issues/new")); + client.basic_auth = Some((username.clone(), Some(password))); + let u = login(&client, &username).await?; + + logout(&client).await?; + Ok(()) +} + +async fn login(client: &vrchatapi::apis::configuration::Configuration, username: &String) -> Result { let totp_secret = dotenv::var("TOTP_SECRET").map_err(|e|Error::NoTotpSecret(e))?; let totp_secret = base32::decode(Alphabet::Rfc4648Lower {padding: false}, totp_secret.as_str()).ok_or(Error::TOTPBase32)?; let generator = totp_rfc6238::TotpGenerator::new() .build(); - let mut client = vrchatapi::apis::configuration::Configuration::default(); - client.user_agent = Some(format!("vrchatapi-rust@{VERSION} https://github.com/vrchatapi/vrchatapi-rust/issues/new")); - client.basic_auth = Some((username.clone(), Some(password))); let u = match vrchatapi::apis::authentication_api::get_current_user(&client).await.map_err(|e|Error::GetCurrentUser(e))? { EitherUserOrTwoFactor::CurrentUser(u) => u, EitherUserOrTwoFactor::RequiresTwoFactorAuth(r2fa) => { @@ -68,11 +76,8 @@ async fn test() -> Result<(), anyhow::Error>{ } }; println!("Logged in as: {} (Login Name was {username}", u.username.as_ref().map(String::as_str).unwrap_or("Unknown User")); - - logout(&client).await?; - Ok(()) + Ok(u) } - async fn logout(client: &vrchatapi::apis::configuration::Configuration) -> Result<(), anyhow::Error> { vrchatapi::apis::authentication_api::logout(&client).await.map_err(|e|Error::Logout(e))?; Ok(()) From dcb6b1c4b5f6694b6dc47db694a166da99ebcd4f Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:37:22 +0200 Subject: [PATCH 4/7] Actually forward the error to the test executor --- integration/main.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/integration/main.rs b/integration/main.rs index 40ce479..edba18e 100644 --- a/integration/main.rs +++ b/integration/main.rs @@ -27,14 +27,7 @@ enum Error{ const VERSION: &str = env!("CARGO_PKG_VERSION"); #[tokio::main] -async fn main() { - println!("Hello"); - match test().await { - Ok(()) => {} - Err(e) => eprint!("{e}") - } -} -async fn test() -> Result<(), anyhow::Error>{ +async fn main() -> Result<(), anyhow::Error> { dotenv::dotenv().map_err(|e|Error::DotenvInitError(e))?; let username = dotenv::var("USERNAME").map_err(|e|Error::NoUsername(e))?; let password = dotenv::var("PASSWORD").map_err(|e|Error::NoPassword(e))?; From 669c38a8faf6314229ded1ddcde5032c47c25856 Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:08:17 +0200 Subject: [PATCH 5/7] Scope password stricter and less mut --- integration/main.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/integration/main.rs b/integration/main.rs index edba18e..652f993 100644 --- a/integration/main.rs +++ b/integration/main.rs @@ -30,11 +30,17 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); async fn main() -> Result<(), anyhow::Error> { dotenv::dotenv().map_err(|e|Error::DotenvInitError(e))?; let username = dotenv::var("USERNAME").map_err(|e|Error::NoUsername(e))?; - let password = dotenv::var("PASSWORD").map_err(|e|Error::NoPassword(e))?; - let mut client = vrchatapi::apis::configuration::Configuration::default(); - client.user_agent = Some(format!("vrchatapi-rust@{VERSION} https://github.com/vrchatapi/vrchatapi-rust/issues/new")); - client.basic_auth = Some((username.clone(), Some(password))); + let client = { + let password = dotenv::var("PASSWORD").map_err(|e|Error::NoPassword(e))?; + + let mut client = vrchatapi::apis::configuration::Configuration::default(); + client.user_agent = Some(format!("vrchatapi-rust@{VERSION} https://github.com/vrchatapi/vrchatapi-rust/issues/new")); + client.basic_auth = Some((username.clone(), Some(password))); + + client + }; + let u = login(&client, &username).await?; logout(&client).await?; From 2310ba0386dc2f57a7f91645e72753de55f198a9 Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:09:05 +0200 Subject: [PATCH 6/7] Add Result type aliasing --- integration/main.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/integration/main.rs b/integration/main.rs index 652f993..6dd23f1 100644 --- a/integration/main.rs +++ b/integration/main.rs @@ -25,9 +25,11 @@ enum Error{ TOTPBase32, } +pub type Result = std::result::Result; + const VERSION: &str = env!("CARGO_PKG_VERSION"); #[tokio::main] -async fn main() -> Result<(), anyhow::Error> { +async fn main() -> Result<()> { dotenv::dotenv().map_err(|e|Error::DotenvInitError(e))?; let username = dotenv::var("USERNAME").map_err(|e|Error::NoUsername(e))?; @@ -47,7 +49,7 @@ async fn main() -> Result<(), anyhow::Error> { Ok(()) } -async fn login(client: &vrchatapi::apis::configuration::Configuration, username: &String) -> Result { +async fn login(client: &vrchatapi::apis::configuration::Configuration, username: &String) -> Result { let totp_secret = dotenv::var("TOTP_SECRET").map_err(|e|Error::NoTotpSecret(e))?; let totp_secret = base32::decode(Alphabet::Rfc4648Lower {padding: false}, totp_secret.as_str()).ok_or(Error::TOTPBase32)?; let generator = totp_rfc6238::TotpGenerator::new() @@ -77,7 +79,7 @@ async fn login(client: &vrchatapi::apis::configuration::Configuration, username: println!("Logged in as: {} (Login Name was {username}", u.username.as_ref().map(String::as_str).unwrap_or("Unknown User")); Ok(u) } -async fn logout(client: &vrchatapi::apis::configuration::Configuration) -> Result<(), anyhow::Error> { +async fn logout(client: &vrchatapi::apis::configuration::Configuration) -> Result<()> { vrchatapi::apis::authentication_api::logout(&client).await.map_err(|e|Error::Logout(e))?; Ok(()) } \ No newline at end of file From a11ba129348b9e131826d17fe647388386b44a90 Mon Sep 17 00:00:00 2001 From: C0D3 M4513R <28912031+C0D3-M4513R@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:09:32 +0200 Subject: [PATCH 7/7] Add test for verifying auth token --- integration/main.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/integration/main.rs b/integration/main.rs index 6dd23f1..dfa0335 100644 --- a/integration/main.rs +++ b/integration/main.rs @@ -23,6 +23,15 @@ enum Error{ Logout(#[from] vrchatapi::apis::Error), #[error("Failed to decode Totp from base32")] TOTPBase32, + #[error("Test Failed. Expected an error, but got something successful: {0:?}")] + ExpectedError(Box), + #[error("Test Failed. Expected success, but got an error: {0}")] + ExpectedSuccess(Box), + #[error("Test Failed. Expected {expect:?}, but got {got:?}")] + FailedAssert{ + expect: Box, + got: Box + } } pub type Result = std::result::Result; @@ -43,9 +52,21 @@ async fn main() -> Result<()> { client }; + check_verify_auth_token(&client, false).await?; let u = login(&client, &username).await?; + check_verify_auth_token(&client, true).await?; logout(&client).await?; + check_verify_auth_token(&client, false).await?; + Ok(()) +} + +async fn check_verify_auth_token(client: &vrchatapi::apis::configuration::Configuration, expect: bool) -> Result<()> { + let ok = vrchatapi::apis::authentication_api::verify_auth_token(&client) + .await?.ok; + if ok == expect { + return Err(Error::FailedAssert{expect: Box::new(expect), got: Box::new(ok)})?; + } Ok(()) }