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
FROM chef as planner
COPY .. .
COPY . .
# Compute a lock-like file for our project
RUN cargo chef prepare --recipe-path recipe.json
@ -12,7 +12,7 @@ FROM chef as builder
COPY --from=planner /app/recipe.json recipe.json
# Build our project dependencies
RUN cargo chef cook --release --recipe-path recipe.json
COPY .. .
COPY . .
ENV SQLX_OFFLINE true
# Build the project
RUN cargo build --release --bin appflowy_cloud

View File

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

View File

@ -1,3 +1,5 @@
use std::time::SystemTime;
use gotrue_entity::OAuthProvider;
use gotrue_entity::OAuthURL;
use reqwest::Method;
@ -112,10 +114,11 @@ impl Client {
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 resp = self
.http_client_with_auth(Method::GET, &url)?
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<AFUserProfileView>::from_response(resp)
@ -123,10 +126,11 @@ impl Client {
.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 resp = self
.http_client_with_auth(Method::GET, &url)?
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<AFWorkspaces>::from_response(resp)
@ -169,10 +173,11 @@ impl Client {
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 resp = self
.http_client_with_auth(Method::POST, &url)?
.http_client_with_auth(Method::POST, &url)
.await?
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()?;
@ -186,7 +191,8 @@ impl Client {
"password": password,
});
let resp = self
.http_client_with_auth(Method::POST, &url)?
.http_client_with_auth(Method::POST, &url)
.await?
.json(&payload)
.send()
.await?;
@ -199,30 +205,33 @@ impl Client {
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 resp = self
.http_client_with_auth(Method::POST, &url)?
.http_client_with_auth(Method::POST, &url)
.await?
.json(&params)
.send()
.await?;
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 resp = self
.http_client_with_auth(Method::PUT, &url)?
.http_client_with_auth(Method::PUT, &url)
.await?
.json(&params)
.send()
.await?;
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 resp = self
.http_client_with_auth(Method::GET, &url)?
.http_client_with_auth(Method::GET, &url)
.await?
.json(&params)
.send()
.await?;
@ -231,10 +240,11 @@ impl Client {
.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 resp = self
.http_client_with_auth(Method::DELETE, &url)?
.http_client_with_auth(Method::DELETE, &url)
.await?
.json(&params)
.send()
.await?;
@ -254,17 +264,34 @@ impl Client {
}
}
fn http_client_with_auth(&self, method: Method, url: &str) -> Result<RequestBuilder, AppError> {
match &self.token {
None => Err(ErrorCode::NotLoggedIn.into()),
Some(t) => {
let request_builder = self
.http_client
.request(method, url)
.bearer_auth(&t.access_token);
Ok(request_builder)
},
async fn http_client_with_auth(
&mut self,
method: Method,
url: &str,
) -> Result<RequestBuilder, AppError> {
let token = self.token().ok_or(ErrorCode::NotLoggedIn)?;
// Refresh token if it's about to expire
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(

View File

@ -1,5 +1,6 @@
use std::fmt::Display;
use std::num::ParseIntError;
use std::time::SystemTimeError;
use std::{borrow::Cow, str};
use serde::{Deserialize, Serialize};
@ -133,3 +134,9 @@ impl From<ParseIntError> for AppError {
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::{
client::utils::{REGISTERED_EMAIL, REGISTERED_PASSWORD, REGISTERED_USER_MUTEX},
client_api_client,
@ -11,5 +15,39 @@ async fn refresh_success() {
let password = &REGISTERED_PASSWORD;
let mut c = client_api_client();
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();
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]
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;
assert!(res.is_err());
}

View File

@ -2,7 +2,7 @@ use client_api::Client;
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()
.await
.unwrap()

View File

@ -17,7 +17,7 @@ async fn success_insert_collab_test() {
.unwrap();
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();
c.create_collab(InsertCollabParams::new(
1,
@ -50,7 +50,7 @@ async fn success_delete_collab_test() {
.unwrap();
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();
c.create_collab(InsertCollabParams::new(
1,
@ -88,7 +88,7 @@ async fn fail_insert_collab_with_empty_payload_test() {
.await
.unwrap();
let workspace_id = workspace_id_from_client(&c).await;
let workspace_id = workspace_id_from_client(&mut c).await;
let error = c
.create_collab(InsertCollabParams::new(
1,

View File

@ -13,7 +13,7 @@ use storage::collab::FLUSH_PER_UPDATE;
async fn realtime_write_collab_test() {
let object_id = uuid::Uuid::new_v4().to_string();
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
for i in 0..=5 {
@ -28,7 +28,7 @@ async fn realtime_write_collab_test() {
test_client.disconnect().await;
assert_collab_json(
&test_client.api_client,
&mut test_client.api_client,
&object_id,
&collab_type,
3,
@ -48,7 +48,7 @@ async fn realtime_write_collab_test() {
async fn one_direction_peer_sync_test() {
let object_id = uuid::Uuid::new_v4().to_string();
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;
// 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(
&client_1.api_client,
&mut client_1.api_client,
&object_id,
&collab_type,
5,
@ -129,13 +129,13 @@ async fn multiple_collab_edit_test() {
let collab_type = CollabType::Document;
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 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 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_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;
assert_collab_json(
&client_1.api_client,
&mut client_1.api_client,
&object_id_1,
&collab_type,
3,
@ -154,7 +154,7 @@ async fn multiple_collab_edit_test() {
.await;
assert_collab_json(
&client_2.api_client,
&mut client_2.api_client,
&object_id_2,
&collab_type,
3,
@ -164,7 +164,7 @@ async fn multiple_collab_edit_test() {
)
.await;
assert_collab_json(
&client_3.api_client,
&mut client_3.api_client,
&object_id_3,
&collab_type,
3,

View File

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