From 7568f49755e28de6076683c59528950189b41d27 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 27 Aug 2023 15:01:22 +0100 Subject: [PATCH] test: add tests to bot routes --- Cargo.lock | 2 + .../database/src/models/messages/model.rs | 2 +- crates/core/models/src/v0/bots.rs | 27 +++- crates/delta/Cargo.toml | 4 + crates/delta/src/routes/bots/create.rs | 32 ++++ crates/delta/src/routes/bots/delete.rs | 42 +++++ crates/delta/src/routes/bots/edit.rs | 40 +++++ crates/delta/src/routes/bots/fetch.rs | 30 ++++ crates/delta/src/routes/bots/fetch_owned.rs | 33 ++++ crates/delta/src/routes/bots/fetch_public.rs | 38 +++++ crates/delta/src/routes/bots/invite.rs | 151 +++++++++++++++--- crates/delta/src/routes/root.rs | 4 +- crates/delta/src/util/test.rs | 106 ++++++++++-- 13 files changed, 473 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 788d0cec..c115abf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3587,6 +3587,8 @@ dependencies = [ "nanoid", "num_enum 0.5.7", "once_cell", + "rand 0.8.5", + "redis-kiss", "regex", "reqwest", "revolt-database", diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index 1e83cd08..92a1c6b6 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -273,7 +273,7 @@ impl Message { ) .await; - todo!() + Ok(()) } /// Append content to message diff --git a/crates/core/models/src/v0/bots.rs b/crates/core/models/src/v0/bots.rs index 6f42b81f..3d1a1df3 100644 --- a/crates/core/models/src/v0/bots.rs +++ b/crates/core/models/src/v0/bots.rs @@ -80,10 +80,16 @@ auto_derived!( /// Bot Username pub username: String, /// Profile Avatar - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))] + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] pub avatar: String, /// Profile Description - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))] + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] pub description: String, } @@ -96,6 +102,7 @@ auto_derived!( } /// Bot Details + #[derive(Default)] #[cfg_attr(feature = "validator", derive(Validate))] pub struct DataCreateBot { /// Bot username @@ -107,6 +114,7 @@ auto_derived!( } /// New Bot Details + #[derive(Default)] #[cfg_attr(feature = "validator", derive(Validate))] pub struct DataEditBot { /// Bot username @@ -130,6 +138,21 @@ auto_derived!( pub remove: Option>, } + /// Where we are inviting a bot to + #[serde(untagged)] + pub enum InviteBotDestination { + /// Invite to a server + Server { + /// Server Id + server: String, + }, + /// Invite to a group + Group { + /// Group Id + group: String, + }, + } + /// Owned Bots Response /// /// Both lists are sorted by their IDs. diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index cee8deb5..d2773653 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -8,6 +8,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# Test +rand = "0.8.5" +redis-kiss = "0.1.4" + # Utility lru = "0.7.0" url = "2.2.2" diff --git a/crates/delta/src/routes/bots/create.rs b/crates/delta/src/routes/bots/create.rs index ab87bee8..f92b98e9 100644 --- a/crates/delta/src/routes/bots/create.rs +++ b/crates/delta/src/routes/bots/create.rs @@ -25,3 +25,35 @@ pub async fn create_bot( let bot = Bot::create(db, info.name, &user, None).await?; Ok(Json(bot.into())) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn create_bot() { + let harness = TestHarness::new().await; + let (_, session, _) = harness.new_user().await; + + let response = harness + .client + .post("/bots/create") + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataCreateBot { + name: TestHarness::rand_string(), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let bot: v0::Bot = response.into_json().await.expect("`Bot`"); + assert!(harness.db.fetch_bot(&bot.id).await.is_ok()); + } +} diff --git a/crates/delta/src/routes/bots/delete.rs b/crates/delta/src/routes/bots/delete.rs index 18616587..b9f4701a 100644 --- a/crates/delta/src/routes/bots/delete.rs +++ b/crates/delta/src/routes/bots/delete.rs @@ -20,3 +20,45 @@ pub async fn delete_bot( bot.delete(db).await.map(|_| EmptyResponse) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{events::client::EventV1, Bot}; + use rocket::http::{Header, Status}; + + #[rocket::async_test] + async fn delete_bot() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + let response = harness + .client + .delete(format!("/bots/{}", bot.id)) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + assert!(harness.db.fetch_bot(&bot.id).await.is_err()); + drop(response); + + let event = harness + .wait_for_event(|event| match event { + EventV1::UserUpdate { id, .. } => id == &bot.id, + _ => false, + }) + .await; + + match event { + EventV1::UserUpdate { data, .. } => { + assert_eq!(data.flags, Some(2)); + } + _ => unreachable!(), + } + } +} diff --git a/crates/delta/src/routes/bots/edit.rs b/crates/delta/src/routes/bots/edit.rs index 4118cbc4..70170177 100644 --- a/crates/delta/src/routes/bots/edit.rs +++ b/crates/delta/src/routes/bots/edit.rs @@ -70,3 +70,43 @@ pub async fn edit_bot( Ok(Json(bot.into())) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::Bot; + use revolt_models::v0::{self, FieldsBot}; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn edit_bot() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + let response = harness + .client + .patch(format!("/bots/{}", bot.id)) + .header(ContentType::JSON) + .body( + json!(v0::DataEditBot { + public: Some(true), + remove: Some(vec![FieldsBot::Token]), + ..Default::default() + }) + .to_string(), + ) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let updated_bot: v0::Bot = response.into_json().await.expect("`Bot`"); + assert!(!bot.public); + assert!(updated_bot.public); + } +} diff --git a/crates/delta/src/routes/bots/fetch.rs b/crates/delta/src/routes/bots/fetch.rs index 811fb60a..932895b4 100644 --- a/crates/delta/src/routes/bots/fetch.rs +++ b/crates/delta/src/routes/bots/fetch.rs @@ -27,3 +27,33 @@ pub async fn fetch_bot( bot: bot.into(), })) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::Bot; + use revolt_models::v0; + use rocket::http::{Header, Status}; + + #[rocket::async_test] + async fn fetch_bot() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + let response = harness + .client + .get(format!("/bots/{}", bot.id)) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let response: v0::FetchBotResponse = response.into_json().await.expect("`Bot`"); + assert_eq!(response.bot, bot.into()); + } +} diff --git a/crates/delta/src/routes/bots/fetch_owned.rs b/crates/delta/src/routes/bots/fetch_owned.rs index c99f8c19..8a40b228 100644 --- a/crates/delta/src/routes/bots/fetch_owned.rs +++ b/crates/delta/src/routes/bots/fetch_owned.rs @@ -28,3 +28,36 @@ pub async fn fetch_owned_bots(db: &State, user: User) -> Result`"); + assert_eq!(resp.bots.len(), 1); + assert_eq!(resp.users.len(), 1); + assert_eq!(resp.bots[0], bot.into()); + assert_eq!(resp.bots[0].id, resp.users[0].id); + } +} diff --git a/crates/delta/src/routes/bots/fetch_public.rs b/crates/delta/src/routes/bots/fetch_public.rs index 6c5ff56a..93ea1e7a 100644 --- a/crates/delta/src/routes/bots/fetch_public.rs +++ b/crates/delta/src/routes/bots/fetch_public.rs @@ -23,3 +23,41 @@ pub async fn fetch_public_bot( let user = db.fetch_user(&bot.id).await?; Ok(Json(bot.into_public_bot(user))) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{Bot, PartialBot}; + use revolt_models::v0; + + #[rocket::async_test] + async fn fetch_public() { + let harness = TestHarness::new().await; + let (_, _, user) = harness.new_user().await; + + let mut bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + bot.update( + &harness.db, + PartialBot { + public: Some(true), + ..Default::default() + }, + vec![], + ) + .await + .unwrap(); + + let bot_user = harness.db.fetch_user(&bot.id).await.expect("`User`"); + let response = harness + .client + .get(format!("/bots/{}/invite", bot.id)) + .dispatch() + .await; + + let public_bot: v0::PublicBot = response.into_json().await.expect("`PublicBot`"); + assert_eq!(public_bot, bot.into_public_bot(bot_user)); + } +} diff --git a/crates/delta/src/routes/bots/invite.rs b/crates/delta/src/routes/bots/invite.rs index 76e64f55..9677881d 100644 --- a/crates/delta/src/routes/bots/invite.rs +++ b/crates/delta/src/routes/bots/invite.rs @@ -1,29 +1,15 @@ use revolt_database::util::permissions::DatabasePermissionQuery; use revolt_database::Member; use revolt_database::{util::reference::Reference, Database, User}; -use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_models::v0; +use revolt_permissions::{ + calculate_channel_permissions, calculate_server_permissions, ChannelPermission, +}; use revolt_result::{create_error, Result}; use rocket::State; use rocket::serde::json::Json; use rocket_empty::EmptyResponse; -use serde::Deserialize; - -/// # Invite Destination -#[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum InviteBotDestination { - /// Invite to a server - Server { - /// Server Id - server: String, - }, - /// Invite to a group - Group { - /// Group Id - group: String, - }, -} /// # Invite Bot /// @@ -34,7 +20,7 @@ pub async fn invite_bot( db: &State, user: User, target: Reference, - dest: Json, + dest: Json, ) -> Result { if user.bot.is_some() { return Err(create_error!(IsBot)); @@ -48,7 +34,7 @@ pub async fn invite_bot( let bot_user = db.fetch_user(&bot.id).await?; match dest.into_inner() { - InviteBotDestination::Server { server } => { + v0::InviteBotDestination::Server { server } => { let server = db.fetch_server(&server).await?; let mut query = DatabasePermissionQuery::new(db, &user).server(&server); @@ -60,11 +46,11 @@ pub async fn invite_bot( .await .map(|_| EmptyResponse) } - InviteBotDestination::Group { group } => { + v0::InviteBotDestination::Group { group } => { let mut channel = db.fetch_channel(&group).await?; let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); - calculate_server_permissions(&mut query) + calculate_channel_permissions(&mut query) .await .throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?; @@ -75,3 +61,124 @@ pub async fn invite_bot( } } } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{events::client::EventV1, Bot, Channel, Server}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn invite_bot_to_group() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + // FIXME: Channel::create_group + let group = Channel::Group { + id: ulid::Ulid::new().to_string(), + name: TestHarness::rand_string(), + owner: user.id.to_string(), + description: None, + last_message_id: None, + icon: None, + nsfw: false, + permissions: None, + recipients: vec![user.id.to_string()], + }; + + group.create(&harness.db).await.unwrap(); + + let response = harness + .client + .post(format!("/bots/{}/invite", bot.id)) + .header(ContentType::JSON) + .body(json!(v0::InviteBotDestination::Group { group: group.id() }).to_string()) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + drop(response); + + let event = harness + .wait_for_event(|event| match event { + EventV1::ChannelGroupJoin { id, .. } => id == &group.id(), + _ => false, + }) + .await; + + match event { + EventV1::ChannelGroupJoin { user, .. } => { + assert_eq!(bot.id, user); + } + _ => unreachable!(), + } + } + + #[rocket::async_test] + async fn invite_bot_to_server() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + // FIXME: Server::create_server + let server = Server { + id: ulid::Ulid::new().to_string(), + name: TestHarness::rand_string(), + owner: user.id.to_string(), + analytics: false, + discoverable: false, + nsfw: false, + banner: None, + icon: None, + categories: None, + channels: vec![], + default_permissions: 0, + description: None, + flags: None, + roles: Default::default(), + system_messages: None, + }; + + server.create(&harness.db).await.unwrap(); + + let response = harness + .client + .post(format!("/bots/{}/invite", bot.id)) + .header(ContentType::JSON) + .body( + json!(v0::InviteBotDestination::Server { + server: server.id.to_string() + }) + .to_string(), + ) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + drop(response); + + let event = harness + .wait_for_event(|event| match event { + EventV1::ServerMemberJoin { id, .. } => id == &server.id, + _ => false, + }) + .await; + + match event { + EventV1::ServerMemberJoin { user, .. } => { + assert_eq!(bot.id, user); + } + _ => unreachable!(), + } + } +} diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs index 978a040b..e223ad8d 100644 --- a/crates/delta/src/routes/root.rs +++ b/crates/delta/src/routes/root.rs @@ -145,14 +145,14 @@ mod test { #[rocket::async_test] async fn hello_world() { let harness = crate::util::test::TestHarness::new().await; - let response = harness.get("/").dispatch().await; + let response = harness.client.get("/").dispatch().await; assert_eq!(response.status(), Status::Ok); } #[rocket::async_test] async fn hello_world_concurrent() { let harness = crate::util::test::TestHarness::new().await; - let response = harness.get("/").dispatch().await; + let response = harness.client.get("/").dispatch().await; assert_eq!(response.status(), Status::Ok); } } diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs index d4567275..cd15ae22 100644 --- a/crates/delta/src/util/test.rs +++ b/crates/delta/src/util/test.rs @@ -1,8 +1,19 @@ +use futures::StreamExt; +use rand::Rng; +use redis_kiss::redis::aio::PubSub; +use revolt_database::{events::client::EventV1, Database, DatabaseInfo, User}; +use revolt_quark::authifier::{ + models::{Account, Session}, + Authifier, +}; use rocket::local::asynchronous::Client; -use std::ops::Deref; pub struct TestHarness { - client: Client, + pub client: Client, + authifier: Authifier, + pub db: Database, + sub: PubSub, + event_buffer: Vec, } impl TestHarness { @@ -13,14 +24,87 @@ impl TestHarness { .await .expect("valid rocket instance"); - TestHarness { client } - } -} - -impl Deref for TestHarness { - type Target = Client; - - fn deref(&self) -> &Self::Target { - &self.client + let db = DatabaseInfo::Auto.connect().await.expect("`Database`"); + let mut sub = redis_kiss::open_pubsub_connection() + .await + .expect("`PubSub`"); + + sub.psubscribe("*").await.unwrap(); + + TestHarness { + client, + authifier: Authifier { + database: db.clone().into(), + config: revolt_quark::util::authifier::config(), + event_channel: None, + }, + db, + sub, + event_buffer: vec![], + } + } + + pub fn rand_string() -> String { + let mut rng = rand::thread_rng(); + (&mut rng) + .sample_iter(rand::distributions::Alphanumeric) + .take(20) + .map(char::from) + .collect() + } + + pub async fn new_user(&self) -> (Account, Session, User) { + let account = Account::new( + &self.authifier, + format!("{}@revolt.chat", TestHarness::rand_string()), + "password".to_string(), + false, + ) + .await + .expect("`Account`"); + + let session = account + .create_session(&self.authifier, String::new()) + .await + .expect("`Session`"); + + let user = User::create( + &self.db, + TestHarness::rand_string(), + account.id.to_string(), + None, + ) + .await + .expect("`User`"); + + (account, session, user) + } + + pub async fn wait_for_event(&mut self, predicate: F) -> EventV1 + where + F: Fn(&EventV1) -> bool, + { + for event in &self.event_buffer { + if predicate(event) { + // does not remove from buffer + return event.clone(); + } + } + + let mut stream = self.sub.on_message(); + while let Some(item) = stream.next().await { + let payload: EventV1 = redis_kiss::decode_payload(&item.unwrap()).unwrap(); + + if predicate(&payload) { + return payload; + } + + self.event_buffer.push(payload); + } + + // WARNING: if predicate is never satisfied, this will never return + // should add a timeout for events so tests can fail gracefully + + unreachable!() } }