feat: returns if user is new for both oauth and password login (#67)

* feat: returns if user is new for both oauth and password login

* test: add test for firt time sign_in and subsequent

* chore: remove uneeded use

* fix: compile

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Zack 2023-09-21 14:05:25 +08:00 committed by GitHub
parent bbc913e45f
commit df9c71edb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 158 additions and 111 deletions

View File

@ -47,14 +47,8 @@ jobs:
docker-compose up -d docker-compose up -d
sleep 5 # sometimes the gotrue server may not be ready yet sleep 5 # sometimes the gotrue server may not be ready yet
source .env
curl localhost:9998/signup \ ./build/init_registered_user.sh
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_1"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_1"'"}' \
--header 'Content-Type: application/json'
source .env
curl localhost:9998/signup \
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_2"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_2"'"}' \
--header 'Content-Type: application/json'
# revert to require signup email verification # revert to require signup email verification
export GOTRUE_MAILER_AUTOCONFIRM=false export GOTRUE_MAILER_AUTOCONFIRM=false

View File

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_user (uuid, email, name)\n SELECT $1, $2, $3\n WHERE NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE email = $2\n )\n AND NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE uuid = $1\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "6638a972a142a500adb842843394d64d5625b0fef0a7c14b1b751bd64567f9b7"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_user (uuid, email, name)\n SELECT $1, $2, $3\n WHERE NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE email = $2\n )\n AND NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE uuid = $1\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "6bbb6f2e06a63df25a7a50624f1931b50c481f29a36b0f9264c1c1d4439f5935"
}

20
build/init_registered_user.sh Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -x
set -eo pipefail
cd "$(dirname "$0")/.."
source .env
# register user 1
curl localhost:9998/signup \
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_1"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_1"'"}' \
--header 'Content-Type: application/json'
# register user 2
curl localhost:9998/signup \
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_2"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_2"'"}' \
--header 'Content-Type: application/json'
# register user 3
curl localhost:9998/signup \
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_3"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_3"'"}' \
--header 'Content-Type: application/json'

View File

@ -46,18 +46,11 @@ pkill -f appflowy_cloud || true
cargo sqlx database create && cargo sqlx migrate run && cargo sqlx prepare --workspace cargo sqlx database create && cargo sqlx migrate run && cargo sqlx prepare --workspace
RUST_LOG=trace cargo run & RUST_LOG=trace cargo run &
# sometimes the gotrue server may not be ready yet # sometimes the gotrue server may not be ready yet
sleep 1 sleep 1
source .env
# register user 1 # created registered user
curl localhost:9998/signup \ ./build/init_registered_user.sh
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_1"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_1"'"}' \
--header 'Content-Type: application/json'
# register user 2
curl localhost:9998/signup \
--data-raw '{"email":"'"$GOTRUE_REGISTERED_EMAIL_2"'","password":"'"$GOTRUE_REGISTERED_PASSWORD_2"'"}' \
--header 'Content-Type: application/json'
# revert to require signup email verification # revert to require signup email verification
export GOTRUE_MAILER_AUTOCONFIRM=false export GOTRUE_MAILER_AUTOCONFIRM=false

View File

@ -24,6 +24,8 @@ GOTRUE_REGISTERED_EMAIL_1=user1@example.com
GOTRUE_REGISTERED_PASSWORD_1=Password123! GOTRUE_REGISTERED_PASSWORD_1=Password123!
GOTRUE_REGISTERED_EMAIL_2=user2@example.com GOTRUE_REGISTERED_EMAIL_2=user2@example.com
GOTRUE_REGISTERED_PASSWORD_2=Password456! GOTRUE_REGISTERED_PASSWORD_2=Password456!
GOTRUE_REGISTERED_EMAIL_3=user3@example.com
GOTRUE_REGISTERED_PASSWORD_3=Password789!
# url to the postgres database # url to the postgres database
DATABASE_URL=postgres://postgres:password@localhost:5433/postgres DATABASE_URL=postgres://postgres:password@localhost:5433/postgres

View File

@ -4,6 +4,8 @@ use reqwest::Method;
use reqwest::RequestBuilder; use reqwest::RequestBuilder;
use shared_entity::data::AppResponse; use shared_entity::data::AppResponse;
use shared_entity::dto::SignInParams; use shared_entity::dto::SignInParams;
use shared_entity::dto::SignInPasswordResponse;
use shared_entity::dto::SignInTokenResponse;
use shared_entity::dto::UserUpdateParams; use shared_entity::dto::UserUpdateParams;
use shared_entity::dto::WorkspaceMembersParams; use shared_entity::dto::WorkspaceMembersParams;
use std::time::SystemTime; use std::time::SystemTime;
@ -41,7 +43,7 @@ impl Client {
} }
// e.g. appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer // e.g. appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer
pub async fn sign_in_url(&mut self, url: &str) -> Result<(), AppError> { pub async fn sign_in_url(&mut self, url: &str) -> Result<bool, AppError> {
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;
let mut expires_in: Option<i64> = None; let mut expires_in: Option<i64> = None;
@ -84,7 +86,7 @@ impl Client {
})?; })?;
let access_token = access_token.ok_or(url_missing_param("access_token"))?; let access_token = access_token.ok_or(url_missing_param("access_token"))?;
let user = self.user_info(&access_token).await?; let (user, new) = self.sign_in_token(&access_token).await?;
self.token.set(AccessTokenResponse { self.token.set(AccessTokenResponse {
access_token, access_token,
@ -97,16 +99,14 @@ impl Client {
provider_refresh_token, provider_refresh_token,
}); });
Ok(()) Ok(new)
} }
pub async fn user_info(&self, access_token: &str) -> Result<User, AppError> { pub async fn sign_in_token(&self, access_token: &str) -> Result<(User, bool), AppError> {
let url = format!("{}/api/user/info/{}", self.base_url, access_token); let url = format!("{}/api/user/sign_in/token/{}", self.base_url, access_token);
let resp = self.http_client.get(&url).send().await?; let resp = self.http_client.get(&url).send().await?;
let user = AppResponse::<User>::from_response(resp) let sign_in_resp: SignInTokenResponse = AppResponse::from_response(resp).await?.into_data()?;
.await? Ok((sign_in_resp.user, sign_in_resp.is_new))
.into_data()?;
Ok(user)
} }
pub fn token(&self) -> Option<&AccessTokenResponse> { pub fn token(&self) -> Option<&AccessTokenResponse> {
@ -205,17 +205,17 @@ impl Client {
Ok(()) Ok(())
} }
pub async fn sign_in_password(&mut self, email: &str, password: &str) -> Result<(), AppError> { pub async fn sign_in_password(&mut self, email: &str, password: &str) -> Result<bool, AppError> {
let url = format!("{}/api/user/sign_in/password", self.base_url); let url = format!("{}/api/user/sign_in/password", self.base_url);
let params = SignInParams { let params = SignInParams {
email: email.to_owned(), email: email.to_owned(),
password: password.to_owned(), password: password.to_owned(),
}; };
let resp = self.http_client.post(&url).json(&params).send().await?; let resp = self.http_client.post(&url).json(&params).send().await?;
self let sign_in_resp: SignInPasswordResponse =
.token AppResponse::from_response(resp).await?.into_data()?;
.set(AppResponse::from_response(resp).await?.into_data()?); self.token.set(sign_in_resp.access_token_resp);
Ok(()) Ok(sign_in_resp.is_new)
} }
pub async fn refresh(&mut self) -> Result<(), AppError> { pub async fn refresh(&mut self) -> Result<(), AppError> {

View File

@ -13,13 +13,13 @@ serde_repr = "0.1.16"
thiserror = "1.0.47" thiserror = "1.0.47"
reqwest = "0.11.18" reqwest = "0.11.18"
uuid = { version = "1.3.3", features = ["v4"] } uuid = { version = "1.3.3", features = ["v4"] }
gotrue-entity = { path = "../gotrue-entity" }
actix-web = { version = "4.4.0", default-features = false, features = ["http2"], optional = true } actix-web = { version = "4.4.0", default-features = false, features = ["http2"], optional = true }
sqlx = { version = "0.7", default-features = false, features = ["postgres"], optional = true } sqlx = { version = "0.7", default-features = false, features = ["postgres"], optional = true }
validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true } validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true }
gotrue-entity = { path = "../gotrue-entity", optional = true }
opener = "0.6.1" opener = "0.6.1"
url = "2.4.1" url = "2.4.1"
[features] [features]
cloud = ["actix-web", "sqlx", "validator", "gotrue-entity"] cloud = ["actix-web", "sqlx", "validator"]

View File

@ -1,5 +1,7 @@
// Data Transfer Objects (DTO) // Data Transfer Objects (DTO)
use gotrue_entity::{AccessTokenResponse, User};
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct WorkspaceMembersParams { pub struct WorkspaceMembersParams {
pub workspace_uuid: uuid::Uuid, pub workspace_uuid: uuid::Uuid,
@ -18,3 +20,15 @@ pub struct UserUpdateParams {
pub password: String, pub password: String,
pub name: Option<String>, pub name: Option<String>,
} }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct SignInPasswordResponse {
pub access_token_resp: AccessTokenResponse,
pub is_new: bool,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct SignInTokenResponse {
pub user: User,
pub is_new: bool,
}

View File

@ -26,28 +26,30 @@ pub async fn update_user_name(
pub async fn create_user_if_not_exists( pub async fn create_user_if_not_exists(
pool: &PgPool, pool: &PgPool,
gotrue_uuid: &uuid::Uuid, user_uuid: &uuid::Uuid,
email: &str, email: &str,
name: &str, name: &str,
) -> Result<(), sqlx::Error> { ) -> Result<bool, sqlx::Error> {
sqlx::query!( let affected_rows = sqlx::query!(
r#" r#"
INSERT INTO af_user (uuid, email, name) INSERT INTO af_user (uuid, email, name)
SELECT $1, $2, $3 SELECT $1, $2, $3
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM public.af_user WHERE email = $2 SELECT 1 FROM public.af_user WHERE email = $2
) )
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM public.af_user WHERE uuid = $1 SELECT 1 FROM public.af_user WHERE uuid = $1
) )
"#, "#,
gotrue_uuid, user_uuid,
email, email,
name name
) )
.execute(pool) .execute(pool)
.await?; .await?
Ok(()) .rows_affected();
Ok(affected_rows > 0)
} }
pub async fn get_user_id(pool: &PgPool, gotrue_uuid: &uuid::Uuid) -> Result<i64, sqlx::Error> { pub async fn get_user_id(pool: &PgPool, gotrue_uuid: &uuid::Uuid) -> Result<i64, sqlx::Error> {

View File

@ -9,7 +9,9 @@ use crate::domain::{UserEmail, UserName, UserPassword};
use crate::state::AppState; use crate::state::AppState;
use gotrue_entity::{AccessTokenResponse, OAuthProvider, OAuthURL, User}; use gotrue_entity::{AccessTokenResponse, OAuthProvider, OAuthURL, User};
use shared_entity::data::{AppResponse, JsonAppResponse}; use shared_entity::data::{AppResponse, JsonAppResponse};
use shared_entity::dto::{SignInParams, UserUpdateParams}; use shared_entity::dto::{
SignInParams, SignInPasswordResponse, SignInTokenResponse, UserUpdateParams,
};
use shared_entity::error::AppError; use shared_entity::error::AppError;
use shared_entity::error_code::ErrorCode; use shared_entity::error_code::ErrorCode;
use storage_entity::AFUserProfileView; use storage_entity::AFUserProfileView;
@ -25,10 +27,10 @@ pub fn user_scope() -> Scope {
// auth server integration // auth server integration
.service(web::resource("/sign_up").route(web::post().to(sign_up_handler))) .service(web::resource("/sign_up").route(web::post().to(sign_up_handler)))
.service(web::resource("/sign_in/password").route(web::post().to(sign_in_password_handler))) .service(web::resource("/sign_in/password").route(web::post().to(sign_in_password_handler)))
.service(web::resource("/sign_in/token/{access_token}").route(web::get().to(sign_in_token_handler)))
.service(web::resource("/sign_out").route(web::post().to(sign_out_handler))) .service(web::resource("/sign_out").route(web::post().to(sign_out_handler)))
.service(web::resource("/update").route(web::post().to(update_handler))) .service(web::resource("/update").route(web::post().to(update_handler)))
.service(web::resource("/oauth/{provider}").route(web::get().to(oauth_handler))) .service(web::resource("/oauth/{provider}").route(web::get().to(oauth_handler)))
.service(web::resource("/info/{access_token}").route(web::get().to(info_handler)))
.service(web::resource("/refresh/{refresh_token}").route(web::get().to(refresh_handler))) .service(web::resource("/refresh/{refresh_token}").route(web::get().to(refresh_handler)))
.service(web::resource("/profile").route(web::get().to(profile_handler))) .service(web::resource("/profile").route(web::get().to(profile_handler)))
@ -48,13 +50,15 @@ async fn refresh_handler(
Ok(AppResponse::Ok().with_data(oauth_url).into()) Ok(AppResponse::Ok().with_data(oauth_url).into())
} }
async fn info_handler( async fn sign_in_token_handler(
path: web::Path<String>, path: web::Path<String>,
state: Data<AppState>, state: Data<AppState>,
) -> Result<JsonAppResponse<User>> { ) -> Result<JsonAppResponse<SignInTokenResponse>> {
let access_token = path.into_inner(); let access_token = path.into_inner();
let user = biz::user::info(&state.pg_pool, &state.gotrue_client, &access_token).await?; let (user, is_new) =
Ok(AppResponse::Ok().with_data(user).into()) biz::user::sign_in_token(&state.pg_pool, &state.gotrue_client, &access_token).await?;
let resp = SignInTokenResponse { user, is_new };
Ok(AppResponse::Ok().with_data(resp).into())
} }
async fn oauth_handler( async fn oauth_handler(
@ -110,17 +114,20 @@ async fn sign_out_handler(
async fn sign_in_password_handler( async fn sign_in_password_handler(
req: Json<SignInParams>, req: Json<SignInParams>,
state: Data<AppState>, state: Data<AppState>,
) -> Result<JsonAppResponse<AccessTokenResponse>> { ) -> Result<JsonAppResponse<SignInPasswordResponse>> {
let req = req.into_inner(); let req = req.into_inner();
let token = biz::user::sign_in( let (token, new) = biz::user::sign_in_password(
&state.pg_pool, &state.pg_pool,
&state.gotrue_client, &state.gotrue_client,
req.email, req.email,
req.password, req.password,
) )
.await?; .await?;
let resp = SignInPasswordResponse {
Ok(AppResponse::Ok().with_data(token).into()) access_token_resp: token,
is_new: new,
};
Ok(AppResponse::Ok().with_data(resp).into())
} }
async fn sign_up_handler( async fn sign_up_handler(
@ -128,13 +135,7 @@ async fn sign_up_handler(
state: Data<AppState>, state: Data<AppState>,
) -> Result<JsonAppResponse<()>> { ) -> Result<JsonAppResponse<()>> {
let req = req.into_inner(); let req = req.into_inner();
biz::user::sign_up( biz::user::sign_up(&state.gotrue_client, req.email, req.password).await?;
&state.pg_pool,
&state.gotrue_client,
req.email,
req.password,
)
.await?;
Ok(AppResponse::Ok().into()) Ok(AppResponse::Ok().into())
} }

View File

@ -28,7 +28,6 @@ pub async fn refresh(
#[instrument(level = "info", skip_all, err)] #[instrument(level = "info", skip_all, err)]
pub async fn sign_up( pub async fn sign_up(
pg_pool: &PgPool,
gotrue_client: &Client, gotrue_client: &Client,
email: String, email: String,
password: String, password: String,
@ -36,23 +35,20 @@ pub async fn sign_up(
validate_email_password(&email, &password)?; validate_email_password(&email, &password)?;
let user = gotrue_client.sign_up(&email, &password).await??; let user = gotrue_client.sign_up(&email, &password).await??;
tracing::info!("user sign up: {:?}", user); tracing::info!("user sign up: {:?}", user);
if user.confirmed_at.is_some() {
let gotrue_uuid = uuid::Uuid::from_str(&user.id)?;
storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &user.email, "").await?;
}
Ok(()) Ok(())
} }
pub async fn info( pub async fn sign_in_token(
pg_pool: &PgPool, pg_pool: &PgPool,
gotrue_client: &Client, gotrue_client: &Client,
access_token: &str, access_token: &str,
) -> Result<User, AppError> { ) -> Result<(User, bool), AppError> {
let user = gotrue_client.user_info(access_token).await??; let user = gotrue_client.user_info(access_token).await??;
let user_uuid = uuid::Uuid::from_str(&user.id)?; let user_uuid = uuid::Uuid::from_str(&user.id)?;
let name: String = name_from_user_metadata(&user.user_metadata); let name: String = name_from_user_metadata(&user.user_metadata);
storage::workspace::create_user_if_not_exists(pg_pool, &user_uuid, &user.email, &name).await?; let new =
Ok(user) storage::workspace::create_user_if_not_exists(pg_pool, &user_uuid, &user.email, &name).await?;
Ok((user, new))
} }
pub async fn oauth(gotrue_client: &Client, provider: OAuthProvider) -> Result<OAuthURL, AppError> { pub async fn oauth(gotrue_client: &Client, provider: OAuthProvider) -> Result<OAuthURL, AppError> {
@ -81,18 +77,19 @@ pub async fn get_profile(
} }
#[instrument(level = "info", skip_all, err)] #[instrument(level = "info", skip_all, err)]
pub async fn sign_in( pub async fn sign_in_password(
pg_pool: &PgPool, pg_pool: &PgPool,
gotrue_client: &Client, gotrue_client: &Client,
email: String, email: String,
password: String, password: String,
) -> Result<AccessTokenResponse, AppError> { ) -> Result<(AccessTokenResponse, bool), AppError> {
let grant = Grant::Password(PasswordGrant { email, password }); let grant = Grant::Password(PasswordGrant { email, password });
let token = gotrue_client.token(&grant).await??; let token = gotrue_client.token(&grant).await??;
let gotrue_uuid = uuid::Uuid::from_str(&token.user.id)?; let gotrue_uuid = uuid::Uuid::from_str(&token.user.id)?;
storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &token.user.email, "") let new =
.await?; storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &token.user.email, "")
Ok(token) .await?;
Ok((token, new))
} }
pub async fn update( pub async fn update(

View File

@ -1,6 +1,6 @@
use shared_entity::error_code::ErrorCode; use shared_entity::error_code::ErrorCode;
use crate::client::utils::{generate_unique_email, REGISTERED_USERS, REGISTERED_USERS_MUTEX}; use crate::client::utils::{generate_unique_email, REGISTERED_USERS};
use crate::client_api_client; use crate::client_api_client;
#[tokio::test] #[tokio::test]
@ -45,20 +45,39 @@ async fn sign_in_unconfirmed_email() {
#[tokio::test] #[tokio::test]
async fn sign_in_success() { async fn sign_in_success() {
let _guard = REGISTERED_USERS_MUTEX.lock().await; {
// First Time
let mut c = client_api_client();
let registered_user = &REGISTERED_USERS[2];
let is_new = c
.sign_in_password(&registered_user.email, &registered_user.password)
.await
.unwrap();
assert!(is_new);
let token = c.token().unwrap();
assert!(token.user.confirmed_at.is_some());
let mut c = client_api_client(); let workspaces = c.workspaces().await.unwrap();
c.sign_in_password(&REGISTERED_USERS[0].email, &REGISTERED_USERS[0].password) assert_eq!(workspaces.0.len(), 1);
.await let profile = c.profile().await.unwrap();
.unwrap(); let latest_workspace = workspaces.get_latest(&profile);
let token = c.token().unwrap(); assert!(latest_workspace.is_some());
assert!(token.user.confirmed_at.is_some()); }
let workspaces = c.workspaces().await.unwrap(); {
assert!(!workspaces.0.is_empty()); // Subsequent Times
let profile = c.profile().await.unwrap(); let mut c = client_api_client();
let latest_workspace = workspaces.get_latest(&profile); let registered_user = &REGISTERED_USERS[2];
assert!(latest_workspace.is_some()); let is_new = c
.sign_in_password(&registered_user.email, &registered_user.password)
.await
.unwrap();
assert!(!is_new);
// workspaces should be the same
let workspaces = c.workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
}
} }
#[tokio::test] #[tokio::test]
@ -66,7 +85,7 @@ async fn sign_in_with_url() {
let url_str = "appflowy-flutter://#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1ODIyMjMsInN1YiI6Ijk5MGM2NDNjLTMyMWEtNGNmMi04OWY1LTNhNmJhZGFjMTg5NCIsImVtYWlsIjoiNG5uaWhpbGF0ZWRAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJnb29nbGUiLCJwcm92aWRlcnMiOlsiZ29vZ2xlIl19LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJlbWFpbCI6IjRubmloaWxhdGVkQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJmdSB6aXhpYW5nIiwiaXNzIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXNlcmluZm8vdjIvbWUiLCJuYW1lIjoiZnUgeml4aWFuZyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSIsInN1YiI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSJ9LCJyb2xlIjoiIn0.I-7j-Tdj62P56zhzEqvBc7cHMldv5MA_MM7xtrBibbE&expires_in=3600&provider_token=ya29.a0AfB_byCovXs1CUiC9_f9VBTupQPsIxwh9aSlOg0PLYJvv1x1zvVfssrQfW6_Aq9no7EKpCzFUCLElOvK1Xz4x4K5r7tug79tr5b1yiOoUMWTeWTXyV61fZHQbZ9vscAiyKYtq5NqYTiytHcQEFlKr7UMfu6BTbKsUwaCgYKAaISARISFQGOcNnC0Vsx2QCAXgYO3XbfcF91WQ0169&refresh_token=Hi3Jc3I_pj9YrexcR91i5g&token_type=bearer"; let url_str = "appflowy-flutter://#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1ODIyMjMsInN1YiI6Ijk5MGM2NDNjLTMyMWEtNGNmMi04OWY1LTNhNmJhZGFjMTg5NCIsImVtYWlsIjoiNG5uaWhpbGF0ZWRAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJnb29nbGUiLCJwcm92aWRlcnMiOlsiZ29vZ2xlIl19LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJlbWFpbCI6IjRubmloaWxhdGVkQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJmdSB6aXhpYW5nIiwiaXNzIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXNlcmluZm8vdjIvbWUiLCJuYW1lIjoiZnUgeml4aWFuZyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSIsInN1YiI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSJ9LCJyb2xlIjoiIn0.I-7j-Tdj62P56zhzEqvBc7cHMldv5MA_MM7xtrBibbE&expires_in=3600&provider_token=ya29.a0AfB_byCovXs1CUiC9_f9VBTupQPsIxwh9aSlOg0PLYJvv1x1zvVfssrQfW6_Aq9no7EKpCzFUCLElOvK1Xz4x4K5r7tug79tr5b1yiOoUMWTeWTXyV61fZHQbZ9vscAiyKYtq5NqYTiytHcQEFlKr7UMfu6BTbKsUwaCgYKAaISARISFQGOcNnC0Vsx2QCAXgYO3XbfcF91WQ0169&refresh_token=Hi3Jc3I_pj9YrexcR91i5g&token_type=bearer";
let mut c = client_api_client(); let mut c = client_api_client();
match c.sign_in_url(url_str).await { match c.sign_in_url(url_str).await {
Ok(()) => panic!("should not be ok"), Ok(_) => panic!("should not be ok"),
Err(e) => { Err(e) => {
assert_eq!(e.code, ErrorCode::OAuthError); assert_eq!(e.code, ErrorCode::OAuthError);
assert!(e assert!(e

View File

@ -6,7 +6,7 @@ use lazy_static::lazy_static;
lazy_static! { lazy_static! {
pub static ref REGISTERED_USERS_MUTEX: Mutex<()> = Mutex::new(()); pub static ref REGISTERED_USERS_MUTEX: Mutex<()> = Mutex::new(());
pub static ref REGISTERED_USERS: [RegisteredUser; 2] = { pub static ref REGISTERED_USERS: [RegisteredUser; 3] = {
dotenv().ok(); dotenv().ok();
[ [
RegisteredUser { RegisteredUser {
@ -17,6 +17,11 @@ lazy_static! {
email: std::env::var("GOTRUE_REGISTERED_EMAIL_2").unwrap(), email: std::env::var("GOTRUE_REGISTERED_EMAIL_2").unwrap(),
password: std::env::var("GOTRUE_REGISTERED_PASSWORD_2").unwrap(), password: std::env::var("GOTRUE_REGISTERED_PASSWORD_2").unwrap(),
}, },
RegisteredUser {
// used for testing user sign up, and if they are new
email: std::env::var("GOTRUE_REGISTERED_EMAIL_3").unwrap(),
password: std::env::var("GOTRUE_REGISTERED_PASSWORD_3").unwrap(),
},
] ]
}; };
} }