mirror of
				https://github.com/Blah-IM/blahrs.git
				synced 2025-10-31 02:31:37 +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 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()); | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 oxalica
						oxalica