Add tests for room join/leave and fix response status code

This commit is contained in:
oxalica 2024-09-09 04:13:10 -04:00
parent 5cd45232f4
commit e40ec6a324
3 changed files with 115 additions and 8 deletions

View file

@ -819,8 +819,8 @@ async fn room_join(
.is_some_and(|attrs| attrs.contains(RoomAttrs::PUBLIC_JOINABLE)); .is_some_and(|attrs| attrs.contains(RoomAttrs::PUBLIC_JOINABLE));
if !is_public_joinable { if !is_public_joinable {
return Err(error_response!( return Err(error_response!(
StatusCode::FORBIDDEN, StatusCode::NOT_FOUND,
"permission_denied", "not_found",
"room does not exists or user is not allowed to join this room", "room does not exists or user is not allowed to join this room",
)); ));
} }
@ -833,14 +833,13 @@ async fn room_join(
", ",
params![user], params![user],
)?; )?;
txn.execute( let updated = txn.execute(
r" r"
INSERT INTO `room_member` (`rid`, `uid`, `permission`) INSERT INTO `room_member` (`rid`, `uid`, `permission`)
SELECT :rid, `uid`, :perm SELECT :rid, `uid`, :perm
FROM `user` FROM `user`
WHERE `userkey` = :userkey WHERE `userkey` = :userkey
ON CONFLICT (`rid`, `uid`) DO UPDATE SET ON CONFLICT (`rid`, `uid`) DO NOTHING
`permission` = :perm
", ",
named_params! { named_params! {
":rid": rid, ":rid": rid,
@ -848,6 +847,13 @@ async fn room_join(
":perm": permission, ":perm": permission,
}, },
)?; )?;
if updated == 0 {
return Err(error_response!(
StatusCode::CONFLICT,
"exists",
"the user is already in the room",
));
}
txn.commit()?; txn.commit()?;
Ok(()) Ok(())
} }

View file

@ -6,8 +6,8 @@ use std::sync::{Arc, LazyLock};
use anyhow::Result; use anyhow::Result;
use blah::types::{ use blah::types::{
get_timestamp, AuthPayload, CreateRoomPayload, Id, MemberPermission, RoomAttrs, RoomMember, get_timestamp, AuthPayload, CreateRoomPayload, Id, MemberPermission, RoomAdminOp,
RoomMemberList, ServerPermission, UserKey, WithSig, RoomAdminPayload, RoomAttrs, RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig,
}; };
use blahd::{ApiError, AppState, Database, RoomList, RoomMetadata}; use blahd::{ApiError, AppState, Database, RoomList, RoomMetadata};
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
@ -31,6 +31,9 @@ fn mock_rng() -> impl RngCore {
rand::rngs::mock::StepRng::new(9, 1) rand::rngs::mock::StepRng::new(9, 1)
} }
#[derive(Debug, Deserialize)]
enum NoContent {}
trait ResultExt { trait ResultExt {
fn expect_api_err(self, status: StatusCode, code: &str); fn expect_api_err(self, status: StatusCode, code: &str);
} }
@ -258,3 +261,97 @@ async fn room_create_get(server: Server, #[case] public: bool) {
.unwrap(); .unwrap();
assert_eq!(got_joined, expect_list(false)); 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::<RoomList>("/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");
}

View file

@ -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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WithItemId<T> { pub struct WithItemId<T> {
pub cid: Id, pub cid: Id,
@ -347,7 +351,7 @@ pub struct RoomMember {
pub struct AuthPayload {} pub struct AuthPayload {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "typ", rename_all = "snake_case")] // `typ` is provided by `RoomAdminOp`.
pub struct RoomAdminPayload { pub struct RoomAdminPayload {
pub room: Id, pub room: Id,
#[serde(flatten)] #[serde(flatten)]