From 2e14dcf129cab5e1c980655971cfb5ff321b0844 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 12 Nov 2023 19:44:49 +0800 Subject: [PATCH] feat: Client api with test util feature (#161) * chore: expose client base url * feat: add test util feature * ci: fix af cloud test * ci: fix af cloud test --- Cargo.toml | 2 +- libs/client-api/Cargo.toml | 3 ++- libs/client-api/src/http.rs | 48 +++++++++++++++++++++++++++++++++---- tests/gotrue/admin.rs | 12 ++++------ tests/user/sign_in.rs | 18 ++++++-------- tests/user/utils.rs | 20 ++++------------ 6 files changed, 63 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 811ae45d..8b433109 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ tempfile = "3.4.0" assert-json-diff = "2.0.2" dotenv = "0.15.0" scraper = "0.17.1" -client-api = { path = "libs/client-api", features = ["collab-sync"] } +client-api = { path = "libs/client-api", features = ["collab-sync", "test_util"] } opener = "0.6.1" image = "0.23.14" diff --git a/libs/client-api/Cargo.toml b/libs/client-api/Cargo.toml index d733b53f..434ffdd6 100644 --- a/libs/client-api/Cargo.toml +++ b/libs/client-api/Cargo.toml @@ -31,7 +31,7 @@ futures-core = "0.3.26" tokio-retry = "0.3" bytes = "1.0" uuid = "1.4.1" -scraper = "0.17.1" +scraper = { version = "0.17.1", optional = true } # collab sync collab = { version = "0.1.0", optional = true } @@ -46,5 +46,6 @@ prost = "0.12.1" [features] collab-sync = ["collab", "yrs", "lib0"] +test_util = ["scraper"] diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 58f06434..db2c2690 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -96,6 +96,18 @@ impl Client { } } + pub fn base_url(&self) -> &str { + &self.base_url + } + + pub fn ws_addr(&self) -> &str { + &self.ws_addr + } + + pub fn gotrue_url(&self) -> &str { + &self.gotrue_client.base_url + } + #[instrument(level = "debug", skip_all, err)] pub fn restore_token(&self, token: &str) -> Result<(), AppResponseError> { if token.is_empty() { @@ -129,6 +141,7 @@ impl Client { /// Attempts to sign in using a URL, extracting and validating the token parameters from the URL fragment. /// It looks like, e.g., `appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer`. /// + /// return a bool indicating if the user is new pub async fn sign_in_with_url(&self, url: &str) -> Result { let mut access_token: Option = None; let mut token_type: Option = None; @@ -230,12 +243,18 @@ impl Client { Ok(url.to_string()) } + /// Generates a sign action link for the specified email address. /// This is only applicable if user token is with admin privilege. - /// This is typically used to generate a sign in url for another user, given the user's email. - /// User then click on the link in the browser, which calls gotrue authentication server, which - /// then redirects to the appflowy-flutter:// + /// This action link is used on web browser to sign in. When user then click the action link in the browser, + /// which calls gotrue authentication server, which then redirects to the appflowy-flutter:// with the authentication token. + /// + /// [Self::extract_sign_in_url] simulates the browser behavior to extract the sign in url. + /// #[instrument(level = "debug", skip_all, err)] - pub async fn generate_sign_in_url(&self, email: &str) -> Result { + pub async fn generate_sign_in_action_link( + &self, + email: &str, + ) -> Result { let admin_user_params: GenerateLinkParams = GenerateLinkParams { email: email.to_string(), ..Default::default() @@ -250,6 +269,27 @@ impl Client { Ok(link_resp.action_link) } + #[cfg(feature = "test_util")] + /// Used to extract the sign in url from the action link + /// Only expose this method for testing + pub async fn extract_sign_in_url(&self, action_link: &str) -> Result { + let resp = reqwest::Client::new().get(action_link).send().await?; + let html = resp.text().await.unwrap(); + + let fragment = scraper::Html::parse_fragment(&html); + let selector = scraper::Selector::parse("a").unwrap(); + let sign_in_url = fragment + .select(&selector) + .next() + .unwrap() + .value() + .attr("href") + .unwrap() + .to_string(); + + Ok(sign_in_url) + } + #[inline] async fn verify_token(&self, access_token: &str) -> Result<(User, bool), AppResponseError> { let user = self.gotrue_client.user_info(access_token).await?; diff --git a/tests/gotrue/admin.rs b/tests/gotrue/admin.rs index 58780b23..d94cff72 100644 --- a/tests/gotrue/admin.rs +++ b/tests/gotrue/admin.rs @@ -6,7 +6,7 @@ use gotrue::{ use crate::{ localhost_client, - user::utils::{extract_sign_in_url, generate_unique_email, ADMIN_USER}, + user::utils::{generate_unique_email, ADMIN_USER}, LOCALHOST_GOTRUE, }; @@ -144,16 +144,12 @@ async fn admin_generate_link_and_user_sign_in_and_invite() { // new user sign in with link, // invite another user through magic link { - let reqwest_client = reqwest::Client::new(); - let resp = reqwest_client - .get(new_user_sign_in_link) - .send() + let client = localhost_client(); + let appflowy_sign_in_url = client + .extract_sign_in_url(&new_user_sign_in_link) .await .unwrap(); - let resp_text = resp.text().await.unwrap(); - let appflowy_sign_in_url = extract_sign_in_url(&resp_text); - let client = localhost_client(); let is_new = client .sign_in_with_url(&appflowy_sign_in_url) .await diff --git a/tests/user/sign_in.rs b/tests/user/sign_in.rs index 3b9841e1..266cbe36 100644 --- a/tests/user/sign_in.rs +++ b/tests/user/sign_in.rs @@ -1,7 +1,6 @@ use crate::localhost_client; use crate::user::utils::{ - extract_sign_in_url, generate_sign_in_url_for_email, generate_unique_email, - generate_unique_registered_user, + generate_sign_in_action_link, generate_unique_email, generate_unique_registered_user, }; use app_error::ErrorCode; @@ -105,16 +104,13 @@ async fn sign_in_with_invalid_url() { async fn sign_in_with_url() { let c = localhost_client(); let email = generate_unique_email(); - let gotrue_url = generate_sign_in_url_for_email(&email) + let action_link = generate_sign_in_action_link(&email) .await - .replacen("/gotrue", "", 1); // compabilitiy with local testing - println!("url: {}", gotrue_url); + .replacen("/gotrue", "", 1); // compatibility with local testing - // simulating url click - let resp = reqwest::Client::new().get(gotrue_url).send().await.unwrap(); - let resp_text = resp.text().await.unwrap(); - let sign_in_url = extract_sign_in_url(&resp_text); + let sign_in_url = c.extract_sign_in_url(action_link.as_str()).await.unwrap(); + println!("url: {}", sign_in_url); - // simulating back to the app with url - let _ = c.sign_in_with_url(&sign_in_url).await.unwrap(); + let is_new = c.sign_in_with_url(&sign_in_url).await.unwrap(); + assert!(is_new); } diff --git a/tests/user/utils.rs b/tests/user/utils.rs index c0e4032e..ee8454a8 100644 --- a/tests/user/utils.rs +++ b/tests/user/utils.rs @@ -1,6 +1,6 @@ use client_api::Client; use dotenv::dotenv; -use scraper::{Html, Selector}; + use sqlx::types::Uuid; use lazy_static::lazy_static; @@ -65,21 +65,11 @@ pub async fn generate_unique_registered_user_client() -> (Client, User) { (registered_user_client, registered_user) } -pub async fn generate_sign_in_url_for_email(email: &str) -> String { +pub async fn generate_sign_in_action_link(email: &str) -> String { setup_log(); let admin_client = admin_user_client().await; - admin_client.generate_sign_in_url(email).await.unwrap() -} - -pub fn extract_sign_in_url(html_str: &str) -> String { - let fragment = Html::parse_fragment(html_str); - let selector = Selector::parse("a").unwrap(); - fragment - .select(&selector) - .next() + admin_client + .generate_sign_in_action_link(email) + .await .unwrap() - .value() - .attr("href") - .unwrap() - .to_string() }