From 5eeb12c29474990649f68bf065ff8ef63227ee79 Mon Sep 17 00:00:00 2001 From: oxalica Date: Tue, 10 Sep 2024 09:20:32 -0400 Subject: [PATCH] test(webapi): test last seen item --- blahd/tests/webapi.rs | 196 +++++++++++++++++++++++++++++++++--------- 1 file changed, 157 insertions(+), 39 deletions(-) diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs index 4397c12..9a346ca 100644 --- a/blahd/tests/webapi.rs +++ b/blahd/tests/webapi.rs @@ -8,10 +8,10 @@ use std::sync::{Arc, LazyLock}; use anyhow::Result; use blah_types::{ get_timestamp, AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission, - RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMember, RoomMemberList, + RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMember, RoomMemberList, RoomMetadata, ServerPermission, UserKey, WithItemId, WithSig, }; -use blahd::{ApiError, AppState, Database, RoomItems, RoomList, RoomMetadata}; +use blahd::{ApiError, AppState, Database, RoomItems, RoomList}; use ed25519_dalek::SigningKey; use futures_util::TryFutureExt; use rand::rngs::mock::StepRng; @@ -36,7 +36,7 @@ fn rng() -> impl RngCore { rand::rngs::mock::StepRng::new(42, 1) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] enum NoContent {} trait ResultExt { @@ -64,13 +64,13 @@ impl Server { format!("http://{}:{}{}", LOCALHOST, self.port, rhs) } - async fn request( + fn request( &self, method: Method, - url: impl fmt::Display, + url: &str, auth: Option<&str>, body: Option, - ) -> Result> { + ) -> impl Future>> + use<'_, Req, Resp> { let mut b = self.client.request(method, self.url(url)); if let Some(auth) = auth { b = b.header(header::AUTHORIZATION, auth); @@ -78,34 +78,35 @@ impl Server { if let Some(body) = &body { b = b.json(body); } - let resp = b.send().await?; - let status = resp.status(); - let resp_str = resp.text().await?; - if !status.is_success() { - #[derive(Deserialize)] - struct Resp { - error: ApiError, + async move { + let resp = b.send().await?; + let status = resp.status(); + let resp_str = resp.text().await?; + + if !status.is_success() { + #[derive(Deserialize)] + struct Resp { + error: ApiError, + } + let Resp { mut error } = serde_json::from_str(&resp_str)?; + error.status = status; + Err(error.into()) + } else if resp_str.is_empty() { + Ok(None) + } else { + Ok(Some(serde_json::from_str(&resp_str)?)) } - let Resp { mut error } = serde_json::from_str(&resp_str)?; - error.status = status; - Err(error.into()) - } else if resp_str.is_empty() { - Ok(None) - } else { - Ok(Some(serde_json::from_str(&resp_str)?)) } } - async fn get( + fn get( &self, - url: impl fmt::Display, + url: &str, auth: Option<&str>, - ) -> Result { - Ok(self - .request(Method::GET, url, auth, None::<()>) - .await? - .unwrap()) + ) -> impl Future> + use<'_, Resp> { + self.request::(Method::GET, url, auth, None) + .map_ok(|resp| resp.unwrap()) } fn create_room( @@ -151,7 +152,7 @@ impl Server { }, }, ); - self.request::<_, NoContent>(Method::POST, format!("/room/{rid}/admin"), None, Some(req)) + self.request::<_, NoContent>(Method::POST, &format!("/room/{rid}/admin"), None, Some(req)) .map_ok(|None| {}) } @@ -166,9 +167,37 @@ impl Server { }, }, ); - self.request::<_, NoContent>(Method::POST, format!("/room/{rid}/admin"), None, Some(req)) + self.request::<_, NoContent>(Method::POST, &format!("/room/{rid}/admin"), None, Some(req)) .map_ok(|None| {}) } + + fn post_chat( + &self, + rid: Id, + key: &SigningKey, + text: &str, + ) -> impl Future>> + use<'_> { + let item = sign( + key, + &mut *self.rng.borrow_mut(), + ChatPayload { + room: rid, + rich_text: text.into(), + }, + ); + async move { + let cid = self + .request::<_, Id>( + Method::POST, + &format!("/room/{rid}/item"), + None, + Some(item.clone()), + ) + .await? + .unwrap(); + Ok(WithItemId { cid, item }) + } + } } #[fixture] @@ -253,7 +282,7 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ // Alice can always access it. let got_meta = server - .get::(format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng))) + .get::(&format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng))) .await .unwrap(); assert_eq!(got_meta, room_meta); @@ -261,7 +290,7 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ // Bob or public can access it when it is public. for auth in [None, Some(auth(&BOB_PRIV, rng))] { let resp = server - .get::(format!("/room/{rid}"), auth.as_deref()) + .get::(&format!("/room/{rid}"), auth.as_deref()) .await; if public { assert_eq!(resp.unwrap(), room_meta); @@ -393,7 +422,7 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { }; let post = |rid: Id, chat: ChatItem| { server - .request::<_, Id>(Method::POST, format!("/room/{rid}/item"), None, Some(chat)) + .request::<_, Id>(Method::POST, &format!("/room/{rid}/item"), None, Some(chat)) .map_ok(|opt| opt.unwrap()) }; @@ -439,7 +468,7 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { // List with default page size. let items = server - .get::(format!("/room/{rid_pub}/item"), None) + .get::(&format!("/room/{rid_pub}/item"), None) .await .unwrap(); assert_eq!( @@ -452,7 +481,7 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { // List with small page size. let items = server - .get::(format!("/room/{rid_pub}/item?top=1"), None) + .get::(&format!("/room/{rid_pub}/item?top=1"), None) .await .unwrap(); assert_eq!( @@ -465,7 +494,10 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { // Second page. let items = server - .get::(format!("/room/{rid_pub}/item?skipToken={cid2}&top=1"), None) + .get::( + &format!("/room/{rid_pub}/item?skipToken={cid2}&top=1"), + None, + ) .await .unwrap(); assert_eq!( @@ -478,7 +510,10 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { // No more. let items = server - .get::(format!("/room/{rid_pub}/item?skipToken={cid1}&top=1"), None) + .get::( + &format!("/room/{rid_pub}/item?skipToken={cid1}&top=1"), + None, + ) .await .unwrap(); assert_eq!(items, RoomItems::default()); @@ -487,14 +522,14 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { // Access without token. server - .get::(format!("/room/{rid_priv}/item"), None) + .get::(&format!("/room/{rid_priv}/item"), None) .await .expect_api_err(StatusCode::NOT_FOUND, "not_found"); // Not a member. server .get::( - format!("/room/{rid_priv}/item"), + &format!("/room/{rid_priv}/item"), Some(&auth(&BOB_PRIV, rng)), ) .await @@ -503,10 +538,93 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) { // Ok. let items = server .get::( - format!("/room/{rid_priv}/item"), + &format!("/room/{rid_priv}/item"), Some(&auth(&ALICE_PRIV, rng)), ) .await .unwrap(); assert_eq!(items, RoomItems::default()); } + +#[rstest] +#[tokio::test] +async fn last_seen_item(server: Server, ref mut rng: impl RngCore) { + let title = "public room"; + let attrs = RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE; + let rid = server.create_room(&ALICE_PRIV, attrs, title).await.unwrap(); + server + .join_room(rid, &BOB_PRIV, MemberPermission::MAX_SELF_ADD) + .await + .unwrap(); + + let alice_chat1 = server.post_chat(rid, &ALICE_PRIV, "alice1").await.unwrap(); + let alice_chat2 = server.post_chat(rid, &ALICE_PRIV, "alice2").await.unwrap(); + + // 2 new items. + let rooms = server + .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .await + .unwrap(); + assert_eq!( + rooms, + RoomList { + rooms: vec![RoomMetadata { + rid, + title: title.into(), + attrs, + last_item: Some(alice_chat2.clone()), + last_seen_cid: None, + unseen_cnt: Some(2), + }], + skip_token: None, + } + ); + + let seen = |key: &SigningKey, cid: Id| { + server.request::( + Method::POST, + &format!("/room/{rid}/item/{cid}/seen"), + Some(&auth(key, &mut *server.rng.borrow_mut())), + None, + ) + }; + + // Mark the first one seen. + seen(&ALICE_PRIV, alice_chat1.cid).await.unwrap(); + + // 1 new item. + let rooms = server + .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .await + .unwrap(); + assert_eq!( + rooms, + RoomList { + rooms: vec![RoomMetadata { + rid, + title: title.into(), + attrs, + last_item: Some(alice_chat2.clone()), + last_seen_cid: Some(alice_chat1.cid), + unseen_cnt: Some(1), + }], + skip_token: None, + } + ); + + // Mark the second one seen. Now there is no new items. + seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap(); + let rooms = server + .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .await + .unwrap(); + assert_eq!(rooms, RoomList::default()); + + // Marking a seen item seen is a no-op. + seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap(); + let rooms = server + .get::("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng))) + .await + .unwrap(); + assert_eq!(rooms, RoomList::default()); +}