From e40ec6a3240e7550bc81352d561be59c73b424a5 Mon Sep 17 00:00:00 2001 From: oxalica Date: Mon, 9 Sep 2024 04:13:10 -0400 Subject: [PATCH] Add tests for room join/leave and fix response status code --- blahd/src/lib.rs | 16 ++++--- blahd/tests/basic.rs | 101 ++++++++++++++++++++++++++++++++++++++++++- src/types.rs | 6 ++- 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/blahd/src/lib.rs b/blahd/src/lib.rs index c1fea34..26029d6 100644 --- a/blahd/src/lib.rs +++ b/blahd/src/lib.rs @@ -819,8 +819,8 @@ async fn room_join( .is_some_and(|attrs| attrs.contains(RoomAttrs::PUBLIC_JOINABLE)); if !is_public_joinable { return Err(error_response!( - StatusCode::FORBIDDEN, - "permission_denied", + StatusCode::NOT_FOUND, + "not_found", "room does not exists or user is not allowed to join this room", )); } @@ -833,14 +833,13 @@ async fn room_join( ", params![user], )?; - txn.execute( + let updated = txn.execute( r" INSERT INTO `room_member` (`rid`, `uid`, `permission`) SELECT :rid, `uid`, :perm FROM `user` WHERE `userkey` = :userkey - ON CONFLICT (`rid`, `uid`) DO UPDATE SET - `permission` = :perm + ON CONFLICT (`rid`, `uid`) DO NOTHING ", named_params! { ":rid": rid, @@ -848,6 +847,13 @@ async fn room_join( ":perm": permission, }, )?; + if updated == 0 { + return Err(error_response!( + StatusCode::CONFLICT, + "exists", + "the user is already in the room", + )); + } txn.commit()?; Ok(()) } diff --git a/blahd/tests/basic.rs b/blahd/tests/basic.rs index 439d9de..f99e339 100644 --- a/blahd/tests/basic.rs +++ b/blahd/tests/basic.rs @@ -6,8 +6,8 @@ use std::sync::{Arc, LazyLock}; use anyhow::Result; use blah::types::{ - get_timestamp, AuthPayload, CreateRoomPayload, Id, MemberPermission, RoomAttrs, RoomMember, - RoomMemberList, ServerPermission, UserKey, WithSig, + get_timestamp, AuthPayload, CreateRoomPayload, Id, MemberPermission, RoomAdminOp, + RoomAdminPayload, RoomAttrs, RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig, }; use blahd::{ApiError, AppState, Database, RoomList, RoomMetadata}; use ed25519_dalek::SigningKey; @@ -31,6 +31,9 @@ fn mock_rng() -> impl RngCore { rand::rngs::mock::StepRng::new(9, 1) } +#[derive(Debug, Deserialize)] +enum NoContent {} + trait ResultExt { fn expect_api_err(self, status: StatusCode, code: &str); } @@ -258,3 +261,97 @@ async fn room_create_get(server: Server, #[case] public: bool) { .unwrap(); assert_eq!(got_joined, expect_list(false)); } + +#[rstest] +#[tokio::test] +async fn room_join_leave(server: Server) { + let rng = &mut mock_rng(); + let rid_pub = create_room( + &server, + &ALICE_PRIV, + rng, + RoomAttrs::PUBLIC_JOINABLE, + "public room", + ) + .await + .unwrap(); + let rid_priv = create_room( + &server, + &ALICE_PRIV, + rng, + RoomAttrs::empty(), + "private room", + ) + .await + .unwrap(); + + let mut join = |rid: Id, key: &SigningKey| { + let req = sign( + key, + rng, + RoomAdminPayload { + room: rid, + op: RoomAdminOp::AddMember { + permission: MemberPermission::MAX_SELF_ADD, + user: UserKey(key.verifying_key().to_bytes()), + }, + }, + ); + server.request::<_, NoContent>(Method::POST, format!("/room/{rid}/admin"), None, Some(req)) + }; + + // Ok. + join(rid_pub, &BOB_PRIV).await.unwrap(); + // Already joined. + join(rid_pub, &BOB_PRIV) + .await + .expect_api_err(StatusCode::CONFLICT, "exists"); + // Not permitted. + join(rid_priv, &BOB_PRIV) + .await + .expect_api_err(StatusCode::NOT_FOUND, "not_found"); + // Not exists. + join(Id::INVALID, &BOB_PRIV) + .await + .expect_api_err(StatusCode::NOT_FOUND, "not_found"); + + // Bob is joined now. + assert_eq!( + server + .get::("/room?filter=joined", Some(&auth(&BOB_PRIV, rng))) + .await + .unwrap() + .rooms + .len(), + 1, + ); + + let mut leave = |rid: Id, key: &SigningKey| { + let req = sign( + key, + rng, + RoomAdminPayload { + room: rid, + op: RoomAdminOp::RemoveMember { + user: UserKey(key.verifying_key().to_bytes()), + }, + }, + ); + server.request::<_, NoContent>(Method::POST, format!("/room/{rid}/admin"), None, Some(req)) + }; + + // Ok. + leave(rid_pub, &BOB_PRIV).await.unwrap(); + // Already left. + leave(rid_pub, &BOB_PRIV) + .await + .expect_api_err(StatusCode::NOT_FOUND, "not_found"); + // Unpermitted and not inside. + leave(rid_priv, &BOB_PRIV) + .await + .expect_api_err(StatusCode::NOT_FOUND, "not_found"); + // Unpermitted and not inside. + leave(Id::INVALID, &BOB_PRIV) + .await + .expect_api_err(StatusCode::NOT_FOUND, "not_found"); +} diff --git a/src/types.rs b/src/types.rs index 876dc0d..96bccb2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -24,6 +24,10 @@ impl fmt::Display for Id { } } +impl Id { + pub const INVALID: Self = Id(i64::MAX); +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WithItemId { pub cid: Id, @@ -347,7 +351,7 @@ pub struct RoomMember { pub struct AuthPayload {} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ", rename_all = "snake_case")] +// `typ` is provided by `RoomAdminOp`. pub struct RoomAdminPayload { pub room: Id, #[serde(flatten)]