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
This commit is contained in:
parent
04989e9485
commit
2e14dcf129
|
|
@ -93,7 +93,7 @@ tempfile = "3.4.0"
|
||||||
assert-json-diff = "2.0.2"
|
assert-json-diff = "2.0.2"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
scraper = "0.17.1"
|
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"
|
opener = "0.6.1"
|
||||||
image = "0.23.14"
|
image = "0.23.14"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ futures-core = "0.3.26"
|
||||||
tokio-retry = "0.3"
|
tokio-retry = "0.3"
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
uuid = "1.4.1"
|
uuid = "1.4.1"
|
||||||
scraper = "0.17.1"
|
scraper = { version = "0.17.1", optional = true }
|
||||||
|
|
||||||
# collab sync
|
# collab sync
|
||||||
collab = { version = "0.1.0", optional = true }
|
collab = { version = "0.1.0", optional = true }
|
||||||
|
|
@ -46,5 +46,6 @@ prost = "0.12.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
collab-sync = ["collab", "yrs", "lib0"]
|
collab-sync = ["collab", "yrs", "lib0"]
|
||||||
|
test_util = ["scraper"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub fn restore_token(&self, token: &str) -> Result<(), AppResponseError> {
|
pub fn restore_token(&self, token: &str) -> Result<(), AppResponseError> {
|
||||||
if token.is_empty() {
|
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.
|
/// 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`.
|
/// 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<bool, AppResponseError> {
|
pub async fn sign_in_with_url(&self, url: &str) -> Result<bool, AppResponseError> {
|
||||||
let mut access_token: Option<String> = None;
|
let mut access_token: Option<String> = None;
|
||||||
let mut token_type: Option<String> = None;
|
let mut token_type: Option<String> = None;
|
||||||
|
|
@ -230,12 +243,18 @@ impl Client {
|
||||||
Ok(url.to_string())
|
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 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.
|
/// This action link is used on web browser to sign in. When user then click the action link in the browser,
|
||||||
/// User then click on the link in the browser, which calls gotrue authentication server, which
|
/// which calls gotrue authentication server, which then redirects to the appflowy-flutter:// with the authentication token.
|
||||||
/// then redirects to the appflowy-flutter://
|
///
|
||||||
|
/// [Self::extract_sign_in_url] simulates the browser behavior to extract the sign in url.
|
||||||
|
///
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn generate_sign_in_url(&self, email: &str) -> Result<String, AppResponseError> {
|
pub async fn generate_sign_in_action_link(
|
||||||
|
&self,
|
||||||
|
email: &str,
|
||||||
|
) -> Result<String, AppResponseError> {
|
||||||
let admin_user_params: GenerateLinkParams = GenerateLinkParams {
|
let admin_user_params: GenerateLinkParams = GenerateLinkParams {
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -250,6 +269,27 @@ impl Client {
|
||||||
Ok(link_resp.action_link)
|
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<String, AppResponseError> {
|
||||||
|
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]
|
#[inline]
|
||||||
async fn verify_token(&self, access_token: &str) -> Result<(User, bool), AppResponseError> {
|
async fn verify_token(&self, access_token: &str) -> Result<(User, bool), AppResponseError> {
|
||||||
let user = self.gotrue_client.user_info(access_token).await?;
|
let user = self.gotrue_client.user_info(access_token).await?;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use gotrue::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
localhost_client,
|
localhost_client,
|
||||||
user::utils::{extract_sign_in_url, generate_unique_email, ADMIN_USER},
|
user::utils::{generate_unique_email, ADMIN_USER},
|
||||||
LOCALHOST_GOTRUE,
|
LOCALHOST_GOTRUE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -144,16 +144,12 @@ async fn admin_generate_link_and_user_sign_in_and_invite() {
|
||||||
// new user sign in with link,
|
// new user sign in with link,
|
||||||
// invite another user through magic link
|
// invite another user through magic link
|
||||||
{
|
{
|
||||||
let reqwest_client = reqwest::Client::new();
|
let client = localhost_client();
|
||||||
let resp = reqwest_client
|
let appflowy_sign_in_url = client
|
||||||
.get(new_user_sign_in_link)
|
.extract_sign_in_url(&new_user_sign_in_link)
|
||||||
.send()
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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
|
let is_new = client
|
||||||
.sign_in_with_url(&appflowy_sign_in_url)
|
.sign_in_with_url(&appflowy_sign_in_url)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::localhost_client;
|
use crate::localhost_client;
|
||||||
use crate::user::utils::{
|
use crate::user::utils::{
|
||||||
extract_sign_in_url, generate_sign_in_url_for_email, generate_unique_email,
|
generate_sign_in_action_link, generate_unique_email, generate_unique_registered_user,
|
||||||
generate_unique_registered_user,
|
|
||||||
};
|
};
|
||||||
use app_error::ErrorCode;
|
use app_error::ErrorCode;
|
||||||
|
|
||||||
|
|
@ -105,16 +104,13 @@ async fn sign_in_with_invalid_url() {
|
||||||
async fn sign_in_with_url() {
|
async fn sign_in_with_url() {
|
||||||
let c = localhost_client();
|
let c = localhost_client();
|
||||||
let email = generate_unique_email();
|
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
|
.await
|
||||||
.replacen("/gotrue", "", 1); // compabilitiy with local testing
|
.replacen("/gotrue", "", 1); // compatibility with local testing
|
||||||
println!("url: {}", gotrue_url);
|
|
||||||
|
|
||||||
// simulating url click
|
let sign_in_url = c.extract_sign_in_url(action_link.as_str()).await.unwrap();
|
||||||
let resp = reqwest::Client::new().get(gotrue_url).send().await.unwrap();
|
println!("url: {}", sign_in_url);
|
||||||
let resp_text = resp.text().await.unwrap();
|
|
||||||
let sign_in_url = extract_sign_in_url(&resp_text);
|
|
||||||
|
|
||||||
// simulating back to the app with url
|
let is_new = c.sign_in_with_url(&sign_in_url).await.unwrap();
|
||||||
let _ = c.sign_in_with_url(&sign_in_url).await.unwrap();
|
assert!(is_new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use client_api::Client;
|
use client_api::Client;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use scraper::{Html, Selector};
|
|
||||||
use sqlx::types::Uuid;
|
use sqlx::types::Uuid;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
@ -65,21 +65,11 @@ pub async fn generate_unique_registered_user_client() -> (Client, User) {
|
||||||
(registered_user_client, registered_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();
|
setup_log();
|
||||||
let admin_client = admin_user_client().await;
|
let admin_client = admin_user_client().await;
|
||||||
admin_client.generate_sign_in_url(email).await.unwrap()
|
admin_client
|
||||||
}
|
.generate_sign_in_action_link(email)
|
||||||
|
.await
|
||||||
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()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.value()
|
|
||||||
.attr("href")
|
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue