test(webapi): test last seen item

This commit is contained in:
oxalica 2024-09-10 09:20:32 -04:00
parent 74c6fa6f6a
commit 5eeb12c294

View file

@ -8,10 +8,10 @@ use std::sync::{Arc, LazyLock};
use anyhow::Result; use anyhow::Result;
use blah_types::{ use blah_types::{
get_timestamp, AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission, 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, ServerPermission, UserKey, WithItemId, WithSig,
}; };
use blahd::{ApiError, AppState, Database, RoomItems, RoomList, RoomMetadata}; use blahd::{ApiError, AppState, Database, RoomItems, RoomList};
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
use futures_util::TryFutureExt; use futures_util::TryFutureExt;
use rand::rngs::mock::StepRng; use rand::rngs::mock::StepRng;
@ -36,7 +36,7 @@ fn rng() -> impl RngCore {
rand::rngs::mock::StepRng::new(42, 1) rand::rngs::mock::StepRng::new(42, 1)
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
enum NoContent {} enum NoContent {}
trait ResultExt { trait ResultExt {
@ -64,13 +64,13 @@ impl Server {
format!("http://{}:{}{}", LOCALHOST, self.port, rhs) format!("http://{}:{}{}", LOCALHOST, self.port, rhs)
} }
async fn request<Req: Serialize, Resp: DeserializeOwned>( fn request<Req: Serialize, Resp: DeserializeOwned>(
&self, &self,
method: Method, method: Method,
url: impl fmt::Display, url: &str,
auth: Option<&str>, auth: Option<&str>,
body: Option<Req>, body: Option<Req>,
) -> Result<Option<Resp>> { ) -> impl Future<Output = Result<Option<Resp>>> + use<'_, Req, Resp> {
let mut b = self.client.request(method, self.url(url)); let mut b = self.client.request(method, self.url(url));
if let Some(auth) = auth { if let Some(auth) = auth {
b = b.header(header::AUTHORIZATION, auth); b = b.header(header::AUTHORIZATION, auth);
@ -78,6 +78,8 @@ impl Server {
if let Some(body) = &body { if let Some(body) = &body {
b = b.json(body); b = b.json(body);
} }
async move {
let resp = b.send().await?; let resp = b.send().await?;
let status = resp.status(); let status = resp.status();
let resp_str = resp.text().await?; let resp_str = resp.text().await?;
@ -96,16 +98,15 @@ impl Server {
Ok(Some(serde_json::from_str(&resp_str)?)) Ok(Some(serde_json::from_str(&resp_str)?))
} }
} }
}
async fn get<Resp: DeserializeOwned>( fn get<Resp: DeserializeOwned>(
&self, &self,
url: impl fmt::Display, url: &str,
auth: Option<&str>, auth: Option<&str>,
) -> Result<Resp> { ) -> impl Future<Output = Result<Resp>> + use<'_, Resp> {
Ok(self self.request::<NoContent, Resp>(Method::GET, url, auth, None)
.request(Method::GET, url, auth, None::<()>) .map_ok(|resp| resp.unwrap())
.await?
.unwrap())
} }
fn create_room( 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| {}) .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| {}) .map_ok(|None| {})
} }
fn post_chat(
&self,
rid: Id,
key: &SigningKey,
text: &str,
) -> impl Future<Output = Result<WithItemId<ChatItem>>> + 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] #[fixture]
@ -253,7 +282,7 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ
// Alice can always access it. // Alice can always access it.
let got_meta = server let got_meta = server
.get::<RoomMetadata>(format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng))) .get::<RoomMetadata>(&format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng)))
.await .await
.unwrap(); .unwrap();
assert_eq!(got_meta, room_meta); 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. // Bob or public can access it when it is public.
for auth in [None, Some(auth(&BOB_PRIV, rng))] { for auth in [None, Some(auth(&BOB_PRIV, rng))] {
let resp = server let resp = server
.get::<RoomMetadata>(format!("/room/{rid}"), auth.as_deref()) .get::<RoomMetadata>(&format!("/room/{rid}"), auth.as_deref())
.await; .await;
if public { if public {
assert_eq!(resp.unwrap(), room_meta); 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| { let post = |rid: Id, chat: ChatItem| {
server 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()) .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. // List with default page size.
let items = server let items = server
.get::<RoomItems>(format!("/room/{rid_pub}/item"), None) .get::<RoomItems>(&format!("/room/{rid_pub}/item"), None)
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -452,7 +481,7 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
// List with small page size. // List with small page size.
let items = server let items = server
.get::<RoomItems>(format!("/room/{rid_pub}/item?top=1"), None) .get::<RoomItems>(&format!("/room/{rid_pub}/item?top=1"), None)
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -465,7 +494,10 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
// Second page. // Second page.
let items = server let items = server
.get::<RoomItems>(format!("/room/{rid_pub}/item?skipToken={cid2}&top=1"), None) .get::<RoomItems>(
&format!("/room/{rid_pub}/item?skipToken={cid2}&top=1"),
None,
)
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -478,7 +510,10 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
// No more. // No more.
let items = server let items = server
.get::<RoomItems>(format!("/room/{rid_pub}/item?skipToken={cid1}&top=1"), None) .get::<RoomItems>(
&format!("/room/{rid_pub}/item?skipToken={cid1}&top=1"),
None,
)
.await .await
.unwrap(); .unwrap();
assert_eq!(items, RoomItems::default()); 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. // Access without token.
server server
.get::<RoomItems>(format!("/room/{rid_priv}/item"), None) .get::<RoomItems>(&format!("/room/{rid_priv}/item"), None)
.await .await
.expect_api_err(StatusCode::NOT_FOUND, "not_found"); .expect_api_err(StatusCode::NOT_FOUND, "not_found");
// Not a member. // Not a member.
server server
.get::<RoomItems>( .get::<RoomItems>(
format!("/room/{rid_priv}/item"), &format!("/room/{rid_priv}/item"),
Some(&auth(&BOB_PRIV, rng)), Some(&auth(&BOB_PRIV, rng)),
) )
.await .await
@ -503,10 +538,93 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
// Ok. // Ok.
let items = server let items = server
.get::<RoomItems>( .get::<RoomItems>(
format!("/room/{rid_priv}/item"), &format!("/room/{rid_priv}/item"),
Some(&auth(&ALICE_PRIV, rng)), Some(&auth(&ALICE_PRIV, rng)),
) )
.await .await
.unwrap(); .unwrap();
assert_eq!(items, RoomItems::default()); 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::<RoomList>("/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::<NoContent, NoContent>(
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::<RoomList>("/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::<RoomList>("/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::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
.await
.unwrap();
assert_eq!(rooms, RoomList::default());
}