diff --git a/src/commands/allowed_dm/mod.rs b/src/commands/allowed_dm/mod.rs index c6a3e2b..535881f 100644 --- a/src/commands/allowed_dm/mod.rs +++ b/src/commands/allowed_dm/mod.rs @@ -5,6 +5,7 @@ use crate::{Context, Error}; /// Configure whether other users can set reminders to your direct messages #[poise::command(slash_command, rename = "dm")] +#[cfg(not(test))] pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> { Ok(()) } diff --git a/src/commands/allowed_dm/set.rs b/src/commands/allowed_dm/set.rs index 4a14acc..a31d7f0 100644 --- a/src/commands/allowed_dm/set.rs +++ b/src/commands/allowed_dm/set.rs @@ -33,6 +33,7 @@ impl Recordable for Options { /// Allow other users to set reminders in your direct messages #[poise::command(slash_command, rename = "allow", identifying_name = "set_allowed_dm")] +#[cfg(not(test))] pub async fn set(ctx: Context<'_>) -> Result<(), Error> { (Options {}).run(ctx).await } diff --git a/src/commands/dashboard.rs b/src/commands/dashboard.rs index 9c970f5..d3baa5d 100644 --- a/src/commands/dashboard.rs +++ b/src/commands/dashboard.rs @@ -31,6 +31,48 @@ impl Recordable for Options { /// Get the link to the web dashboard #[poise::command(slash_command, rename = "dashboard", identifying_name = "dashboard")] +#[cfg(not(test))] pub async fn command(ctx: Context<'_>) -> Result<(), Error> { (Options {}).run(ctx).await } + +#[cfg(test)] +mod test { + use std::env; + + use sqlx::Pool; + use tokio::sync::{broadcast, Mutex}; + + use crate::{ + commands::dashboard::Options, + test::{MockCache, TestContext, TestData}, + utils::Recordable, + Data, + }; + + #[tokio::test] + async fn dashboard_command() { + let (tx, _rx) = broadcast::channel(16); + let database = Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")) + .await + .unwrap(); + + let ctx = TestContext { + data: &Data { database, popular_timezones: vec![], _broadcast: tx }, + cache: &MockCache {}, + test_data: &Mutex::new(TestData { replies: vec![] }), + shard_id: 0, + }; + + let res = (Options {}).run(ctx).await; + + assert!(res.is_ok(), "command OK"); + assert_eq!(ctx.test_data.lock().await.replies.len(), 1, "one message sent"); + assert!( + ctx.sent_content() + .await + .contains(&String::from("**https://beta.reminder-bot.com/dashboard**")), + "content correct" + ); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6c2ef00..0c24c58 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,21 +1,41 @@ +#[cfg(not(test))] pub mod allowed_dm; +#[cfg(not(test))] mod autocomplete; +#[cfg(not(test))] pub mod clock; +#[cfg(not(test))] pub mod clock_context_menu; +#[cfg(not(test))] pub mod command_macro; pub mod dashboard; +#[cfg(not(test))] pub mod delete; +#[cfg(not(test))] pub mod donate; +#[cfg(not(test))] pub mod help; +#[cfg(not(test))] pub mod info; +#[cfg(not(test))] pub mod look; +#[cfg(not(test))] pub mod multiline; +#[cfg(not(test))] pub mod nudge; +#[cfg(not(test))] pub mod offset; +#[cfg(not(test))] pub mod pause; +#[cfg(not(test))] pub mod remind; +#[cfg(not(test))] pub mod settings; +#[cfg(not(test))] pub mod timer; +#[cfg(not(test))] pub mod timezone; +#[cfg(not(test))] pub mod todo; +#[cfg(not(test))] pub mod webhook; diff --git a/src/main.rs b/src/main.rs index d778235..d1b05c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,18 @@ extern crate lazy_static; mod commands; +#[cfg(not(test))] mod component_models; mod consts; +#[cfg(not(test))] mod event_handlers; +#[cfg(not(test))] mod hooks; mod interval_parser; +#[cfg(not(test))] mod models; +#[cfg(test)] +mod test; mod time_parser; mod utils; @@ -33,6 +39,9 @@ use poise::serenity_prelude::{ use sqlx::{MySql, Pool}; use tokio::sync::{broadcast, broadcast::Sender, RwLock}; +#[cfg(test)] +use crate::test::TestContext; +#[cfg(not(test))] use crate::{ commands::{ allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard, @@ -48,11 +57,18 @@ use crate::{ type Database = MySql; type Error = Box; + +#[cfg(test)] +type Context<'a> = TestContext<'a>; + +#[cfg(not(test))] type Context<'a> = poise::Context<'a, Data, Error>; + type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>; pub struct Data { database: Pool, + #[cfg(not(test))] recording_macros: RwLock>, popular_timezones: Vec, _broadcast: Sender<()>, @@ -81,6 +97,7 @@ impl Display for Ended { impl StdError for Ended {} #[tokio::main(flavor = "multi_thread")] +#[cfg(not(test))] async fn main() -> Result<(), Box> { let (tx, mut rx) = broadcast::channel(16); @@ -90,6 +107,7 @@ async fn main() -> Result<(), Box> { } } +#[cfg(not(test))] async fn _main(tx: Sender<()>) -> Result<(), Box> { env_logger::init(); diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..548aae6 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,95 @@ +use poise::{ + serenity_prelude::{GuildId, UserId}, + CreateReply, +}; +use serde_json::Value; +use tokio::sync::Mutex; + +use crate::{Data, Error}; + +pub(crate) struct TestData { + pub(crate) replies: Vec, +} + +#[derive(Copy, Clone)] +pub(crate) struct TestContext<'a> { + pub(crate) data: &'a Data, + pub(crate) cache: &'a MockCache, + pub(crate) test_data: &'a Mutex, + pub(crate) shard_id: usize, +} + +pub(crate) struct MockUser { + pub(crate) id: UserId, +} + +pub(crate) struct MockCache {} + +impl<'a> TestContext<'a> { + pub async fn say(&self, message: impl Into) -> Result<(), Error> { + self.test_data.lock().await.replies.push(CreateReply::default().content(message)); + + Ok(()) + } + + pub async fn send(&self, reply: CreateReply) -> Result<(), Error> { + self.test_data.lock().await.replies.push(reply.clone()); + + Ok(()) + } + + pub fn guild_id(&self) -> Option { + Some(GuildId::new(1)) + } + + pub async fn defer_ephemeral(&self) -> Result<(), Error> { + Ok(()) + } + + pub fn author(&self) -> MockUser { + MockUser { id: UserId::new(1) } + } + + pub fn data(&self) -> &Data { + return &self.data; + } + + pub fn serenity_context(&self) -> &Self { + return &self; + } + + pub async fn sent_content(&self) -> Vec { + let data = self.test_data.lock().await; + + data.replies + .iter() + .map(|r| { + let reply = r.clone(); + let content = reply.content.unwrap_or(String::new()); + let embed_content = reply + .embeds + .iter() + .map(|e| { + let map = serde_json::to_value(e).unwrap(); + let description = + map.get("description").cloned().unwrap_or(Value::String(String::new())); + return format!("{}", description.as_str().unwrap()); + }) + .collect::>() + .join("\n"); + + return if content.is_empty() { + embed_content + } else { + format!("{}\n{}", content, embed_content) + }; + }) + .collect::>() + } +} + +impl MockCache { + pub fn shard_count(&self) -> usize { + return 1; + } +}