Skip to content

Commit

Permalink
feature: complete chat update and delete model and api handler
Browse files Browse the repository at this point in the history
  • Loading branch information
luffy2025 committed Aug 6, 2024
1 parent 8987d5a commit ff1ae37
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 19 deletions.
2 changes: 2 additions & 0 deletions chat_server/fixtures/test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ VALUES (
'user5',
'$argon2id$v=19$m=19456,t=2,p=1$uSA/eb4JlZTu5o0OOP7axw$aEDst9wVRgHtPaqRySrpuBFuNHK2ncwTl7W7O4dEJ/c'
);
-- update workspace owner_id
UPDATE workspaces SET owner_id=1 WHERE id=1;
-- insert 4 chats
-- insert public/private channel
INSERT INTO chats (ws_id, name, type, members)
Expand Down
8 changes: 8 additions & 0 deletions chat_server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ pub enum AppError {
#[error("create chat error: {0}")]
CreateChatError(String),

#[error("update chat error: {0}")]
UpdateChatError(String),

#[error("delete chat error: {0}")]
DeleteChatError(String),

#[error("sql error: {0}")]
SqlxError(#[from] sqlx::Error),

Expand Down Expand Up @@ -48,6 +54,8 @@ impl IntoResponse for AppError {
AppError::HttpHeaderError(_) => StatusCode::UNPROCESSABLE_ENTITY,
AppError::JWTError(_) => StatusCode::FORBIDDEN,
AppError::CreateChatError(_) => StatusCode::BAD_REQUEST,
AppError::UpdateChatError(_) => StatusCode::BAD_REQUEST,
AppError::DeleteChatError(_) => StatusCode::BAD_REQUEST,
};

(status, Json(ErrorOutput::new(self.to_string()))).into_response()
Expand Down
100 changes: 91 additions & 9 deletions chat_server/src/handlers/chat.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
error::AppError,
models::{Chat, CreateChat},
models::{Chat, CreateChat, UpdateChat, Workspace},
AppState, User,
};
use axum::{
Expand Down Expand Up @@ -29,24 +29,58 @@ pub(crate) async fn create_chat_handler(

pub(crate) async fn get_chat_handler(
State(state): State<AppState>,
Path(id): Path<u64>,
Path(id): Path<i64>,
) -> Result<impl IntoResponse, AppError> {
let chat = Chat::get_by_id(id, &state.pool).await?;
Ok((StatusCode::OK, Json(chat)))
}

pub(crate) async fn update_chat_handler() -> Result<impl IntoResponse, AppError> {
Ok((StatusCode::OK, Json("")))
pub(crate) async fn update_chat_handler(
Extension(user): Extension<User>,
State(state): State<AppState>,
Path(id): Path<i64>,
Json(input): Json<UpdateChat>,
) -> Result<impl IntoResponse, AppError> {
let chat = Chat::get_by_id(id, &state.pool).await?;
if chat.ws_id != user.ws_id {
return Err(AppError::UpdateChatError(
"Can not update the chat which in other workspace.".to_string(),
));
}

let chat = Chat::update(id, input, &state.pool).await?;
Ok((StatusCode::OK, Json(chat)))
}

pub(crate) async fn delete_chat_handler() -> Result<impl IntoResponse, AppError> {
Ok((StatusCode::OK, Json("")))
pub(crate) async fn delete_chat_handler(
Extension(user): Extension<User>,
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<impl IntoResponse, AppError> {
let chat = Chat::get_by_id(id, &state.pool).await?;
match Workspace::find_by_id(chat.ws_id, &state.pool).await? {
Some(ws) => {
if ws.owner_id != user.id {
return Err(AppError::DeleteChatError(
"Only workspace owner can delete the chat.".to_string(),
));
}
}
_ => {
return Err(AppError::DeleteChatError(
"Workspace of chat not exist.".to_string(),
));
}
}

Chat::delete(id, &state.pool).await?;
Ok((StatusCode::OK, Json("success".to_string())))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::AppConfig;
use crate::{models::ChatType, AppConfig};
use anyhow::{Ok, Result};
use http_body_util::BodyExt;

Expand All @@ -55,16 +89,64 @@ mod tests {
let config = AppConfig::load()?;
let (_tdb, state) = AppState::new_for_test(config).await?;

let id: u64 = 1;
let id: i64 = 1;
let ret = get_chat_handler(State(state), Path(id))
.await
.into_response();
assert_eq!(ret.status(), StatusCode::OK);
let body = ret.into_body().collect().await?.to_bytes();
let ret = serde_json::from_slice::<Chat>(&body)?;
assert_eq!(ret.id as u64, id);
assert_eq!(ret.id, id);
assert_eq!(ret.members.len(), 5);

Ok(())
}

#[tokio::test]
async fn chat_update_chat_handler_should_work() -> Result<()> {
let config = AppConfig::load()?;
let (_tdb, state) = AppState::new_for_test(config).await?;

let user = User::find_by_email("[email protected]", &state.pool)
.await?
.unwrap();
let input = UpdateChat::new("pub", &[1, 2, 3], true);
let ret = update_chat_handler(Extension(user), State(state), Path(1), Json(input))
.await?
.into_response();

assert_eq!(ret.status(), StatusCode::OK);
let body = ret.into_body().collect().await?.to_bytes();
let chat = serde_json::from_slice::<Chat>(&body)?;
assert_eq!(chat.name.unwrap(), "pub");
assert_eq!(chat.r#type, ChatType::PublicChannel);
assert_eq!(chat.members.len(), 3);

Ok(())
}

#[tokio::test]
async fn chat_update_chat_handler_should_not_work() -> Result<()> {
let config = AppConfig::load()?;
let (_tdb, state) = AppState::new_for_test(config).await?;

let user = User::new(10, "test_user", "[email protected]");
let input = UpdateChat::new("pub", &[1, 2, 3], true);
let ret = update_chat_handler(Extension(user), State(state), Path(1), Json(input)).await;
assert!(ret.is_err());

Ok(())
}

#[tokio::test]
async fn chat_delete_chat_handler_should_not_work() -> Result<()> {
let config = AppConfig::load()?;
let (_tdb, state) = AppState::new_for_test(config).await?;

let user = User::new(10, "test_user", "[email protected]");
let ret = delete_chat_handler(Extension(user), State(state), Path(1)).await;
assert!(ret.is_err());

Ok(())
}
}
138 changes: 133 additions & 5 deletions chat_server/src/models/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ pub struct CreateChat {
pub public: bool,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateChat {
pub name: Option<String>,
pub members: Vec<i64>,
pub public: bool,
}

impl Chat {
#[allow(unused)]
pub async fn create(input: CreateChat, ws_id: u64, pool: &PgPool) -> Result<Self, AppError> {
let len = input.members.len();
if len < 2 {
Expand Down Expand Up @@ -63,7 +69,6 @@ impl Chat {
Ok(chat)
}

#[allow(unused)]
pub async fn fetch_all(ws_id: u64, pool: &PgPool) -> Result<Vec<Self>, AppError> {
let chats = sqlx::query_as(
r#"
Expand All @@ -79,21 +84,92 @@ impl Chat {
Ok(chats)
}

#[allow(unused)]
pub async fn get_by_id(id: u64, pool: &PgPool) -> Result<Self, AppError> {
pub async fn get_by_id(id: i64, pool: &PgPool) -> Result<Self, AppError> {
let chat = sqlx::query_as(
r#"
SELECT id, ws_id, name, type, members, created_at
FROM chats
WHERE id = $1
"#,
)
.bind(id as i64)
.bind(id)
.fetch_one(pool)
.await?;

Ok(chat)
}

pub async fn update(id: i64, input: UpdateChat, pool: &PgPool) -> Result<Self, AppError> {
let len = input.members.len();
if len < 2 {
return Err(AppError::UpdateChatError(
"Chat must have at least 2 members".to_string(),
));
}
if len > 8 && input.name.is_none() {
return Err(AppError::UpdateChatError(
"Group chat with more than 8 members must have a name".to_string(),
));
}

let users = ChatUser::fetch_by_ids(&input.members, pool).await?;
if users.len() != len {
return Err(AppError::UpdateChatError(
"Some members do not exist.".to_string(),
));
}

let chat_type = match (&input.name, len) {
(None, 2) => ChatType::Single,
(None, _) => ChatType::Group,
(_, _) => {
if input.public {
ChatType::PublicChannel
} else {
ChatType::PrivateChannel
}
}
};

let chat = sqlx::query_as(
r#"
UPDATE chats SET name=$2, type=$3, members=$4
WHERE id=$1
RETURNING id, ws_id, name, type, members, created_at
"#,
)
.bind(id)
.bind(input.name)
.bind(chat_type)
.bind(input.members)
.fetch_one(pool)
.await?;

Ok(chat)
}

pub async fn delete(id: i64, pool: &PgPool) -> Result<(), AppError> {
if id == 0 {
return Err(AppError::DeleteChatError(
"Chat with id=0 can not be delete".to_string(),
));
}

let ret = sqlx::query("DELETE FROM chats WHERE id=$1")
.bind(id)
.execute(pool)
.await?
.rows_affected();

if ret < 1 {
return Err(AppError::DeleteChatError(format!(
"Chat with id={} not exist.",
id
)));
}

Ok(())
}
}

#[cfg(test)]
Expand All @@ -113,6 +189,23 @@ impl CreateChat {
}
}

#[cfg(test)]
impl UpdateChat {
pub fn new(name: &str, members: &[i64], public: bool) -> Self {
let name = if name.is_empty() {
None
} else {
Some(name.to_string())
};

Self {
name,
members: members.to_vec(),
public,
}
}
}

#[cfg(test)]
mod tests {
use anyhow::{Ok, Result};
Expand Down Expand Up @@ -168,4 +261,39 @@ mod tests {
assert_eq!(chats.len(), 4);
Ok(())
}

#[tokio::test]
async fn update_chat_to_public_named_should_work() -> Result<()> {
let (_tdb, pool) = get_test_pool(None).await;

let chat = Chat::get_by_id(3, &pool).await?;
assert!(chat.name.is_none());
assert_eq!(chat.members.len(), 2);
assert_eq!(chat.r#type, ChatType::Single);

let id = 3;
let input = UpdateChat::new("pub", &[1, 2, 3], true);
let chat = Chat::update(id, input, &pool).await?;
assert_eq!(chat.ws_id, 1);
assert_eq!(chat.name.unwrap(), "pub");
assert_eq!(chat.members.len(), 3);
assert_eq!(chat.r#type, ChatType::PublicChannel);

Ok(())
}

#[tokio::test]
async fn chat_delete_should_work() -> Result<()> {
let (_tdb, pool) = get_test_pool(None).await;

let ret = Chat::delete(1, &pool).await;
assert!(ret.is_ok());

let ret = Chat::delete(0, &pool).await;
assert!(ret.is_err());
let ret = Chat::delete(10, &pool).await;
assert!(ret.is_err());

Ok(())
}
}
2 changes: 1 addition & 1 deletion chat_server/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod chat;
mod user;
mod workspace;

pub use chat::CreateChat;
pub use chat::{CreateChat, UpdateChat};
pub use user::{CreateUser, SigninUser};

use chrono::{DateTime, Utc};
Expand Down
5 changes: 2 additions & 3 deletions chat_server/src/models/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,21 @@ impl Workspace {
}

#[allow(unused)]
pub async fn find_by_id(id: u64, pool: &PgPool) -> Result<Option<Self>, AppError> {
pub async fn find_by_id(id: i64, pool: &PgPool) -> Result<Option<Self>, AppError> {
let ws = sqlx::query_as(
r#"
SELECT id, name, owner_id, created_at
FROM workspaces
WHERE id = $1
"#,
)
.bind(id as i64)
.bind(id)
.fetch_optional(pool)
.await?;

Ok(ws)
}

#[allow(unused)]
pub async fn fetch_all_chat_users(id: u64, pool: &PgPool) -> Result<Vec<ChatUser>, AppError> {
let users = sqlx::query_as(
r#"
Expand Down
Loading

0 comments on commit ff1ae37

Please sign in to comment.