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 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<Req: Serialize, Resp: DeserializeOwned>(
fn request<Req: Serialize, Resp: DeserializeOwned>(
&self,
method: Method,
url: impl fmt::Display,
url: &str,
auth: Option<&str>,
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));
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<Resp: DeserializeOwned>(
fn get<Resp: DeserializeOwned>(
&self,
url: impl fmt::Display,
url: &str,
auth: Option<&str>,
) -> Result<Resp> {
Ok(self
.request(Method::GET, url, auth, None::<()>)
.await?
.unwrap())
) -> impl Future<Output = Result<Resp>> + use<'_, Resp> {
self.request::<NoContent, Resp>(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<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]
@ -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::<RoomMetadata>(format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng)))
.get::<RoomMetadata>(&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::<RoomMetadata>(format!("/room/{rid}"), auth.as_deref())
.get::<RoomMetadata>(&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::<RoomItems>(format!("/room/{rid_pub}/item"), None)
.get::<RoomItems>(&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::<RoomItems>(format!("/room/{rid_pub}/item?top=1"), None)
.get::<RoomItems>(&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::<RoomItems>(format!("/room/{rid_pub}/item?skipToken={cid2}&top=1"), None)
.get::<RoomItems>(
&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::<RoomItems>(format!("/room/{rid_pub}/item?skipToken={cid1}&top=1"), None)
.get::<RoomItems>(
&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::<RoomItems>(format!("/room/{rid_priv}/item"), None)
.get::<RoomItems>(&format!("/room/{rid_priv}/item"), None)
.await
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
// Not a member.
server
.get::<RoomItems>(
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::<RoomItems>(
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::<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());
}