mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
test(webapi): test last seen item
This commit is contained in:
parent
74c6fa6f6a
commit
5eeb12c294
1 changed files with 157 additions and 39 deletions
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue