feat: Autorefresh (#44)

* feat: use gotrue from source instead of docker hub image

* test: fix test due to gotrue upgrade

* fix: update prod docker-compose

* chore: cargo fmt --all

* chore: cargo fmt --all

* feat: autorefresh

* test: add test case and auto refresh scenario
This commit is contained in:
Zack 2023-09-15 11:21:05 +08:00 committed by GitHub
parent 939ea29c3b
commit 7345da7c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 117 additions and 45 deletions

View File

@ -4,7 +4,7 @@ WORKDIR /app
RUN apt update && apt install lld clang -y RUN apt update && apt install lld clang -y
FROM chef as planner FROM chef as planner
COPY .. . COPY . .
# Compute a lock-like file for our project # Compute a lock-like file for our project
RUN cargo chef prepare --recipe-path recipe.json RUN cargo chef prepare --recipe-path recipe.json
@ -12,7 +12,7 @@ FROM chef as builder
COPY --from=planner /app/recipe.json recipe.json COPY --from=planner /app/recipe.json recipe.json
# Build our project dependencies # Build our project dependencies
RUN cargo chef cook --release --recipe-path recipe.json RUN cargo chef cook --release --recipe-path recipe.json
COPY .. . COPY . .
ENV SQLX_OFFLINE true ENV SQLX_OFFLINE true
# Build the project # Build the project
RUN cargo build --release --bin appflowy_cloud RUN cargo build --release --bin appflowy_cloud

View File

@ -55,7 +55,7 @@ services:
- APP__GOTRUE__JWT_SECRET=${GOTRUE_JWT_SECRET} - APP__GOTRUE__JWT_SECRET=${GOTRUE_JWT_SECRET}
build: build:
context: . context: .
dockerfile: docker/Dockerfile dockerfile: Dockerfile
image: appflowy_cloud:${BACKEND_VERSION:-latest} image: appflowy_cloud:${BACKEND_VERSION:-latest}
depends_on: depends_on:
- redis - redis

View File

@ -1,3 +1,5 @@
use std::time::SystemTime;
use gotrue_entity::OAuthProvider; use gotrue_entity::OAuthProvider;
use gotrue_entity::OAuthURL; use gotrue_entity::OAuthURL;
use reqwest::Method; use reqwest::Method;
@ -112,10 +114,11 @@ impl Client {
Ok(()) Ok(())
} }
pub async fn profile(&self) -> Result<AFUserProfileView, AppError> { pub async fn profile(&mut self) -> Result<AFUserProfileView, AppError> {
let url = format!("{}/api/user/profile", self.base_url); let url = format!("{}/api/user/profile", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::GET, &url)? .http_client_with_auth(Method::GET, &url)
.await?
.send() .send()
.await?; .await?;
AppResponse::<AFUserProfileView>::from_response(resp) AppResponse::<AFUserProfileView>::from_response(resp)
@ -123,10 +126,11 @@ impl Client {
.into_data() .into_data()
} }
pub async fn workspaces(&self) -> Result<AFWorkspaces, AppError> { pub async fn workspaces(&mut self) -> Result<AFWorkspaces, AppError> {
let url = format!("{}/api/user/workspaces", self.base_url); let url = format!("{}/api/user/workspaces", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::GET, &url)? .http_client_with_auth(Method::GET, &url)
.await?
.send() .send()
.await?; .await?;
AppResponse::<AFWorkspaces>::from_response(resp) AppResponse::<AFWorkspaces>::from_response(resp)
@ -169,10 +173,11 @@ impl Client {
Ok(()) Ok(())
} }
pub async fn sign_out(&self) -> Result<(), AppError> { pub async fn sign_out(&mut self) -> Result<(), AppError> {
let url = format!("{}/api/user/sign_out", self.base_url); let url = format!("{}/api/user/sign_out", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::POST, &url)? .http_client_with_auth(Method::POST, &url)
.await?
.send() .send()
.await?; .await?;
AppResponse::<()>::from_response(resp).await?.into_error()?; AppResponse::<()>::from_response(resp).await?.into_error()?;
@ -186,7 +191,8 @@ impl Client {
"password": password, "password": password,
}); });
let resp = self let resp = self
.http_client_with_auth(Method::POST, &url)? .http_client_with_auth(Method::POST, &url)
.await?
.json(&payload) .json(&payload)
.send() .send()
.await?; .await?;
@ -199,30 +205,33 @@ impl Client {
Ok(()) Ok(())
} }
pub async fn create_collab(&self, params: InsertCollabParams) -> Result<(), AppError> { pub async fn create_collab(&mut self, params: InsertCollabParams) -> Result<(), AppError> {
let url = format!("{}/api/collab/", self.base_url); let url = format!("{}/api/collab/", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::POST, &url)? .http_client_with_auth(Method::POST, &url)
.await?
.json(&params) .json(&params)
.send() .send()
.await?; .await?;
AppResponse::<()>::from_response(resp).await?.into_error() AppResponse::<()>::from_response(resp).await?.into_error()
} }
pub async fn update_collab(&self, params: InsertCollabParams) -> Result<(), AppError> { pub async fn update_collab(&mut self, params: InsertCollabParams) -> Result<(), AppError> {
let url = format!("{}/api/collab/", self.base_url); let url = format!("{}/api/collab/", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::PUT, &url)? .http_client_with_auth(Method::PUT, &url)
.await?
.json(&params) .json(&params)
.send() .send()
.await?; .await?;
AppResponse::<()>::from_response(resp).await?.into_error() AppResponse::<()>::from_response(resp).await?.into_error()
} }
pub async fn get_collab(&self, params: QueryCollabParams) -> Result<RawData, AppError> { pub async fn get_collab(&mut self, params: QueryCollabParams) -> Result<RawData, AppError> {
let url = format!("{}/api/collab/", self.base_url); let url = format!("{}/api/collab/", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::GET, &url)? .http_client_with_auth(Method::GET, &url)
.await?
.json(&params) .json(&params)
.send() .send()
.await?; .await?;
@ -231,10 +240,11 @@ impl Client {
.into_data() .into_data()
} }
pub async fn delete_collab(&self, params: DeleteCollabParams) -> Result<(), AppError> { pub async fn delete_collab(&mut self, params: DeleteCollabParams) -> Result<(), AppError> {
let url = format!("{}/api/collab/", self.base_url); let url = format!("{}/api/collab/", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::DELETE, &url)? .http_client_with_auth(Method::DELETE, &url)
.await?
.json(&params) .json(&params)
.send() .send()
.await?; .await?;
@ -254,17 +264,34 @@ impl Client {
} }
} }
fn http_client_with_auth(&self, method: Method, url: &str) -> Result<RequestBuilder, AppError> { async fn http_client_with_auth(
match &self.token { &mut self,
None => Err(ErrorCode::NotLoggedIn.into()), method: Method,
Some(t) => { url: &str,
let request_builder = self ) -> Result<RequestBuilder, AppError> {
.http_client let token = self.token().ok_or(ErrorCode::NotLoggedIn)?;
.request(method, url)
.bearer_auth(&t.access_token); // Refresh token if it's about to expire
Ok(request_builder) let time_now_sec = SystemTime::now()
}, .duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
if time_now_sec + 60 > token.expires_at {
// Add 60 seconds buffer
self.refresh().await?;
} }
let access_token = self
.token()
.ok_or(ErrorCode::NotLoggedIn)?
.access_token
.as_str();
let request_builder = self
.http_client
.request(method, url)
.bearer_auth(access_token);
Ok(request_builder)
} }
// pub async fn change_password( // pub async fn change_password(

View File

@ -1,5 +1,6 @@
use std::fmt::Display; use std::fmt::Display;
use std::num::ParseIntError; use std::num::ParseIntError;
use std::time::SystemTimeError;
use std::{borrow::Cow, str}; use std::{borrow::Cow, str};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -133,3 +134,9 @@ impl From<ParseIntError> for AppError {
AppError::new(ErrorCode::InvalidUrl, value.to_string()) AppError::new(ErrorCode::InvalidUrl, value.to_string())
} }
} }
impl From<SystemTimeError> for AppError {
fn from(value: SystemTimeError) -> Self {
AppError::new(ErrorCode::Unhandled, value.to_string())
}
}

View File

@ -1,3 +1,7 @@
use std::time::SystemTime;
use gotrue_entity::AccessTokenResponse;
use crate::{ use crate::{
client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX}, client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX},
client_api_client, client_api_client,
@ -11,5 +15,39 @@ async fn refresh_success() {
let password = &REGISTERED_PASSWORD; let password = &REGISTERED_PASSWORD;
let mut c = client_api_client(); let mut c = client_api_client();
c.sign_in_password(email, password).await.unwrap(); c.sign_in_password(email, password).await.unwrap();
let old_token = c.token().unwrap().access_token.to_owned();
std::thread::sleep(std::time::Duration::from_secs(2));
c.refresh().await.unwrap(); c.refresh().await.unwrap();
let new_token = c.token().unwrap().access_token.to_owned();
assert_ne!(old_token, new_token);
}
#[tokio::test]
async fn refresh_trigger() {
let _guard = REGISTERED_USER_MUTEX.lock().await;
let email = &REGISTERED_EMAIL;
let password = &REGISTERED_PASSWORD;
let mut c = client_api_client();
c.sign_in_password(email, password).await.unwrap();
std::thread::sleep(std::time::Duration::from_secs(2));
let token = c.token().unwrap();
let old_access_token = token.access_token.to_owned();
// Set the token to be expired
unsafe {
let token_mut = token as *const AccessTokenResponse as *mut AccessTokenResponse;
token_mut.as_mut().unwrap().expires_at = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
};
// querying that requires auth should trigger a refresh
let _workspaces = c.workspaces().await.unwrap();
let new_token = c.token().unwrap().access_token.to_owned();
assert_ne!(old_access_token, new_token);
} }

View File

@ -3,7 +3,7 @@ use crate::client_api_client;
#[tokio::test] #[tokio::test]
async fn sign_out_but_not_sign_in() { async fn sign_out_but_not_sign_in() {
let c = client_api_client(); let mut c = client_api_client();
let res = c.sign_out().await; let res = c.sign_out().await;
assert!(res.is_err()); assert!(res.is_err());
} }

View File

@ -2,7 +2,7 @@ use client_api::Client;
mod storage_test; mod storage_test;
pub(crate) async fn workspace_id_from_client(c: &Client) -> String { pub(crate) async fn workspace_id_from_client(c: &mut Client) -> String {
c.workspaces() c.workspaces()
.await .await
.unwrap() .unwrap()

View File

@ -17,7 +17,7 @@ async fn success_insert_collab_test() {
.unwrap(); .unwrap();
let raw_data = "hello world".to_string().as_bytes().to_vec(); let raw_data = "hello world".to_string().as_bytes().to_vec();
let workspace_id = workspace_id_from_client(&c).await; let workspace_id = workspace_id_from_client(&mut c).await;
let object_id = Uuid::new_v4().to_string(); let object_id = Uuid::new_v4().to_string();
c.create_collab(InsertCollabParams::new( c.create_collab(InsertCollabParams::new(
1, 1,
@ -50,7 +50,7 @@ async fn success_delete_collab_test() {
.unwrap(); .unwrap();
let raw_data = "hello world".to_string().as_bytes().to_vec(); let raw_data = "hello world".to_string().as_bytes().to_vec();
let workspace_id = workspace_id_from_client(&c).await; let workspace_id = workspace_id_from_client(&mut c).await;
let object_id = Uuid::new_v4().to_string(); let object_id = Uuid::new_v4().to_string();
c.create_collab(InsertCollabParams::new( c.create_collab(InsertCollabParams::new(
1, 1,
@ -88,7 +88,7 @@ async fn fail_insert_collab_with_empty_payload_test() {
.await .await
.unwrap(); .unwrap();
let workspace_id = workspace_id_from_client(&c).await; let workspace_id = workspace_id_from_client(&mut c).await;
let error = c let error = c
.create_collab(InsertCollabParams::new( .create_collab(InsertCollabParams::new(
1, 1,

View File

@ -13,7 +13,7 @@ use storage::collab::FLUSH_PER_UPDATE;
async fn realtime_write_collab_test() { async fn realtime_write_collab_test() {
let object_id = uuid::Uuid::new_v4().to_string(); let object_id = uuid::Uuid::new_v4().to_string();
let collab_type = CollabType::Document; let collab_type = CollabType::Document;
let test_client = TestClient::new(&object_id, collab_type.clone()).await; let mut test_client = TestClient::new(&object_id, collab_type.clone()).await;
// Edit the collab // Edit the collab
for i in 0..=5 { for i in 0..=5 {
@ -28,7 +28,7 @@ async fn realtime_write_collab_test() {
test_client.disconnect().await; test_client.disconnect().await;
assert_collab_json( assert_collab_json(
&test_client.api_client, &mut test_client.api_client,
&object_id, &object_id,
&collab_type, &collab_type,
3, 3,
@ -48,7 +48,7 @@ async fn realtime_write_collab_test() {
async fn one_direction_peer_sync_test() { async fn one_direction_peer_sync_test() {
let object_id = uuid::Uuid::new_v4().to_string(); let object_id = uuid::Uuid::new_v4().to_string();
let collab_type = CollabType::Document; let collab_type = CollabType::Document;
let client_1 = TestClient::new(&object_id, collab_type.clone()).await; let mut client_1 = TestClient::new(&object_id, collab_type.clone()).await;
let client_2 = TestClient::new(&object_id, collab_type.clone()).await; let client_2 = TestClient::new(&object_id, collab_type.clone()).await;
// Edit the collab from client 1 and then the server will broadcast to client 2 // Edit the collab from client 1 and then the server will broadcast to client 2
@ -58,7 +58,7 @@ async fn one_direction_peer_sync_test() {
} }
assert_collab_json( assert_collab_json(
&client_1.api_client, &mut client_1.api_client,
&object_id, &object_id,
&collab_type, &collab_type,
5, 5,
@ -129,13 +129,13 @@ async fn multiple_collab_edit_test() {
let collab_type = CollabType::Document; let collab_type = CollabType::Document;
let object_id_1 = uuid::Uuid::new_v4().to_string(); let object_id_1 = uuid::Uuid::new_v4().to_string();
let client_1 = TestClient::new(&object_id_1, collab_type.clone()).await; let mut client_1 = TestClient::new(&object_id_1, collab_type.clone()).await;
let object_id_2 = uuid::Uuid::new_v4().to_string(); let object_id_2 = uuid::Uuid::new_v4().to_string();
let client_2 = TestClient::new(&object_id_2, collab_type.clone()).await; let mut client_2 = TestClient::new(&object_id_2, collab_type.clone()).await;
let object_id_3 = uuid::Uuid::new_v4().to_string(); let object_id_3 = uuid::Uuid::new_v4().to_string();
let client_3 = TestClient::new(&object_id_3, collab_type.clone()).await; let mut client_3 = TestClient::new(&object_id_3, collab_type.clone()).await;
client_1.collab.lock().insert("title", "I am client 1"); client_1.collab.lock().insert("title", "I am client 1");
client_2.collab.lock().insert("title", "I am client 2"); client_2.collab.lock().insert("title", "I am client 2");
@ -143,7 +143,7 @@ async fn multiple_collab_edit_test() {
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(2)).await;
assert_collab_json( assert_collab_json(
&client_1.api_client, &mut client_1.api_client,
&object_id_1, &object_id_1,
&collab_type, &collab_type,
3, 3,
@ -154,7 +154,7 @@ async fn multiple_collab_edit_test() {
.await; .await;
assert_collab_json( assert_collab_json(
&client_2.api_client, &mut client_2.api_client,
&object_id_2, &object_id_2,
&collab_type, &collab_type,
3, 3,
@ -164,7 +164,7 @@ async fn multiple_collab_edit_test() {
) )
.await; .await;
assert_collab_json( assert_collab_json(
&client_3.api_client, &mut client_3.api_client,
&object_id_3, &object_id_3,
&collab_type, &collab_type,
3, 3,

View File

@ -94,7 +94,7 @@ impl TestClient {
#[allow(dead_code)] #[allow(dead_code)]
pub async fn assert_collab_json( pub async fn assert_collab_json(
client: &client_api::Client, client: &mut client_api::Client,
object_id: &str, object_id: &str,
collab_type: &CollabType, collab_type: &CollabType,
secs: u64, secs: u64,
@ -141,7 +141,7 @@ pub async fn assert_collab_json(
#[allow(dead_code)] #[allow(dead_code)]
pub async fn get_collab_json_from_server( pub async fn get_collab_json_from_server(
client: &client_api::Client, client: &mut client_api::Client,
object_id: &str, object_id: &str,
collab_type: CollabType, collab_type: CollabType,
) -> serde_json::Value { ) -> serde_json::Value {