use crate::{
comments::Comment,
config::Config,
errors::Error,
format,
projects::Project,
tasks::{self, FormatType, SortOrder, Task, priority::Priority},
todoist,
};
use futures::{StreamExt, TryStreamExt, future, stream};
use std::collections::HashSet;
use std::fmt::Display;
use tokio::{fs, io::AsyncReadExt, task::JoinError};
#[derive(Clone)]
pub enum Flag {
Project(Project),
Filter(String),
}
impl Display for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Flag::Project(project) => write!(f, "{project}"),
Flag::Filter(filter) => write!(f, "'{filter}'"),
}
}
}
pub async fn view(config: &mut Config, flag: Flag, sort: &SortOrder) -> Result<String, Error> {
let list_of_tasks = match &flag {
Flag::Project(project) => vec![(
project.name.clone(),
todoist::all_tasks_by_project(config, project, None).await?,
)],
Flag::Filter(filter) => todoist::all_tasks_by_filters(config, filter).await?,
};
let mut buffer = String::new();
for (query, tasks) in list_of_tasks {
let title = format!("Tasks for {query}");
buffer.push('\n');
buffer.push_str(&format::green_string(&title));
buffer.push('\n');
for task in tasks::sort(tasks, config, *sort) {
let comments = Vec::new();
let text = task.fmt(comments, config, FormatType::List, true).await?;
buffer.push('\n');
buffer.push_str(&text);
}
}
Ok(buffer)
}
pub async fn fetch_tasks_by_flag<F, P>(
config: &Config,
flag: &Flag,
project_filter: P,
filter_filter: F,
) -> Result<Vec<Task>, Error>
where
P: Fn(&Task) -> bool,
F: Fn(&Task) -> bool,
{
let tasks = match flag {
Flag::Project(project) => todoist::all_tasks_by_project(config, project, None)
.await?
.into_iter()
.filter(|task| project_filter(task))
.collect::<Vec<Task>>(),
Flag::Filter(filter) => todoist::all_tasks_by_filters(config, filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks)
.filter(|task| filter_filter(task))
.collect::<Vec<Task>>(),
};
Ok(tasks)
}
pub async fn prioritize(config: &Config, flag: Flag, sort: &SortOrder) -> Result<String, Error> {
let project_filter = |task: &Task| task.priority == Priority::None;
let filter_filter = |_task: &Task| true;
let tasks = fetch_tasks_by_flag(config, &flag, project_filter, filter_filter).await?;
let empty_text = format!("No tasks for {flag}");
let success = format!("Successfully prioritized {flag}");
if tasks.is_empty() {
return Ok(format::green_string(&empty_text));
}
let tasks = tasks::sort(tasks, config, *sort);
let handles = stream::iter(tasks)
.then(|task| async {
println!();
tasks::set_priority(config, task, true).await
})
.try_collect::<Vec<_>>()
.await?;
future::join_all(handles).await;
Ok(format::green_string(&success))
}
pub async fn remind(config: &Config, flag: Flag, sort: &SortOrder) -> Result<String, Error> {
let reminder_task_ids = todoist::all_reminders(config, None)
.await?
.into_iter()
.map(|r| r.item_id)
.collect::<HashSet<String>>();
let filter = |task: &Task| !reminder_task_ids.contains(&task.id);
let tasks = fetch_tasks_by_flag(config, &flag, filter, filter).await?;
if tasks.is_empty() {
let empty_text = format!("No tasks for {flag}");
return Ok(format::green_string(&empty_text));
}
let tasks = tasks::sort(tasks, config, *sort);
let handles = stream::iter(tasks)
.then(|task| async {
println!();
tasks::create_reminder(config, task).await
})
.try_collect::<Vec<_>>()
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>();
future::join_all(handles).await;
let success = format!("Successfully reminded {flag}");
Ok(format::green_string(&success))
}
pub async fn timebox(config: &Config, flag: Flag, sort: &SortOrder) -> Result<String, Error> {
let project_filter = |task: &Task| task.duration.is_none();
let filter_filter = |_task: &Task| true;
let tasks = fetch_tasks_by_flag(config, &flag, project_filter, filter_filter).await?;
let empty_text = format!("No tasks for {flag}");
let success = format!("Successfully timeboxed {flag}");
if tasks.is_empty() {
return Ok(format::green_string(&empty_text));
}
let tasks = tasks::sort(tasks, config, *sort);
let mut task_count = i32::try_from(tasks.len())?;
let mut handles = Vec::new();
for task in tasks {
println!();
match tasks::timebox_task(&config.reload().await?, task, &mut task_count, false).await? {
Some(handle) => handles.push(handle),
None => return Ok(format::green_string("Exited")),
}
}
future::join_all(handles).await;
Ok(format::green_string(&success))
}
pub async fn process(config: &Config, flag: Flag, sort: &SortOrder) -> Result<String, Error> {
let project_filter = |task: &Task| {
task.is_today(config).unwrap_or_default()
|| task.has_no_date()
|| task.is_overdue(config).unwrap_or_default()
};
let filter_filter = |_task: &Task| true;
let tasks = fetch_tasks_by_flag(config, &flag, project_filter, filter_filter).await?;
let with_project = match &flag {
Flag::Project(..) => false,
Flag::Filter(..) => true,
};
let tasks = tasks::reject_parent_tasks(tasks, config).await;
let empty_text = format!("No tasks for {flag}");
let success = format!("Successfully processed {flag}");
if tasks.is_empty() {
return Ok(format::green_string(&empty_text));
}
let tasks = tasks::sort(tasks, config, *sort);
let mut task_count = i32::try_from(tasks.len())?;
let tasks_with_comments = fetch_comments_for_tasks(tasks, config).await;
let mut handles = Vec::new();
for task_with_comments in tasks_with_comments {
match process_task_with_comments(task_with_comments, config, &mut task_count, with_project)
.await?
{
ProcessTaskOutcome::Handle(handle) => handles.push(handle),
ProcessTaskOutcome::Exit => return Ok(format::green_string("Exited")),
ProcessTaskOutcome::Skip => {}
}
}
future::join_all(handles).await;
Ok(format::green_string(&success))
}
enum ProcessTaskOutcome {
Handle(tokio::task::JoinHandle<()>),
Exit,
Skip,
}
async fn process_task_with_comments(
task_with_comments: Result<(Task, Result<Vec<Comment>, Error>), JoinError>,
config: &Config,
task_count: &mut i32,
with_project: bool,
) -> Result<ProcessTaskOutcome, Error> {
let (task, comments, with_project) = match task_with_comments {
Ok((task, Ok(comments))) => (task, comments, with_project),
Ok((task, Err(Error { message, source }))) => {
println!("Could not fetch comments from {source}: {message}");
(task, Vec::new(), false)
}
Err(JoinError { .. }) => {
println!("JoinError");
return Ok(ProcessTaskOutcome::Skip);
}
};
println!();
match tasks::process_task(
comments,
&config.reload().await?,
task,
task_count,
with_project,
)
.await?
{
Some(handle) => Ok(ProcessTaskOutcome::Handle(handle)),
None => Ok(ProcessTaskOutcome::Exit),
}
}
async fn fetch_comments_for_tasks(
tasks: Vec<Task>,
config: &Config,
) -> Vec<Result<(Task, Result<Vec<Comment>, Error>), JoinError>> {
let handles = tasks
.into_iter()
.map(|task| {
let config = config.clone();
tokio::spawn(async move {
let comments = todoist::all_comments(&config, &task.id, None).await;
(task, comments)
})
})
.collect::<Vec<_>>();
future::join_all(handles).await
}
pub async fn label(
config: &Config,
flag: Flag,
labels: &[String],
sort: &SortOrder,
) -> Result<String, Error> {
let filter = |_task: &Task| true;
let tasks = fetch_tasks_by_flag(config, &flag, filter, filter).await?;
let empty_text = format!("No tasks for {flag}");
let success = format!("Successfully labeled {flag}");
if tasks.is_empty() {
return Ok(format::green_string(&empty_text));
}
let tasks = tasks::sort(tasks, config, *sort);
let handles = stream::iter(tasks)
.then(|task| async {
println!();
tasks::label_task(config, task, labels).await
})
.try_collect::<Vec<_>>()
.await?;
future::join_all(handles).await;
Ok(format::green_string(&success))
}
pub async fn import(config: &Config, file_path: &str) -> Result<String, Error> {
let mut lines = String::new();
fs::File::open(file_path)
.await?
.read_to_string(&mut lines)
.await?;
let lines: Vec<String> = lines
.split('\n')
.map(std::borrow::ToOwned::to_owned)
.filter(|s| !s.is_empty())
.collect();
for line in lines {
todoist::quick_create_task(config, &line, None).await?;
}
Ok("✓".into())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test;
use crate::test::responses::ResponseFromFile;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn test_import_creates_14_tasks() {
let mut server = mockito::Server::new_async().await;
let import_file = "tests/inputs/import_tasks.txt";
let import_qty = 14;
let mock = server
.mock("POST", "/api/v1/tasks/quick")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTask.read().await)
.expect(import_qty)
.create_async()
.await;
let config = test::fixtures::config().await.with_mock_url(server.url());
assert_eq!(import(&config, import_file).await, Ok(String::from("✓")));
mock.assert();
}
#[tokio::test]
async fn test_prioritize() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock2 = server
.mock("POST", "/api/v1/tasks/6Xqhv4cwxgjwG9w8")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(1);
let filter = String::from("today");
let sort = &SortOrder::Value;
let result = prioritize(&config, Flag::Filter(filter), sort).await;
assert_eq!(result, Ok(String::from("Successfully prioritized 'today'")));
mock.assert();
mock2.assert();
}
#[tokio::test]
async fn test_timebox() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/?project_id=123&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasksWithoutDuration.read().await)
.create_async()
.await;
let mock2 = server
.mock("POST", "/api/v1/tasks/999999")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTask.read().await)
.create_async()
.await;
let mock4 = server
.mock(
"GET",
"/api/v1/comments/?task_id=6Xqhv4cwxgjwG9w8&limit=200",
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::CommentsAllTypes.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(1)
.with_mock_string("tod")
.create()
.await
.expect("expected value or result, got None or Err");
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let sort = &SortOrder::Value;
let result = timebox(&config, Flag::Project(project), sort).await;
assert_matches!(result, Ok(x) if x.contains("Successfully timeboxed"));
let config = config.mock_select(2);
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let result = timebox(&config, Flag::Project(project), sort).await;
assert_matches!(result, Ok(x) if x.contains("Successfully timeboxed"));
let config = config.mock_select(3);
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let result = timebox(&config, Flag::Project(project.clone()), sort).await;
assert_matches!(result, Ok(x) if x.contains("Successfully timeboxed"));
let result = timebox(&config, Flag::Project(project), sort).await;
assert_matches!(result, Ok(x) if x.contains("Successfully timeboxed"));
mock.expect(2);
mock2.expect(2);
mock4.expect(1);
}
#[tokio::test]
async fn test_timebox_returns_exited_when_quit_is_selected() {
let mut server = mockito::Server::new_async().await;
let tasks_mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasksWithoutDuration.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(4)
.create()
.await
.expect("config should be created");
let result = timebox(
&config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await;
assert_eq!(result, Ok("Exited".to_string()));
tasks_mock.assert();
}
#[tokio::test]
async fn test_prioritize_tasks_with_no_tasks() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/?project_id=123&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let config = test::fixtures::config().await.with_mock_url(server.url());
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let sort = &SortOrder::Value;
let result = prioritize(&config, Flag::Project(project), sort).await;
assert_eq!(
result,
Ok(String::from(
"No tasks for myproject\nhttps://app.todoist.com/app/project/123"
))
);
mock.assert();
}
#[tokio::test]
async fn test_empty_task_lists_return_messages() {
let mut server = mockito::Server::new_async().await;
let reminders_mock = server
.mock("GET", "/api/v1/reminders?limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"results":[],"next_cursor":null}"#)
.create_async()
.await;
let tasks_mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"results":[],"next_cursor":null}"#)
.expect(3)
.create_async()
.await;
let config = test::fixtures::config().await.with_mock_url(server.url());
assert_eq!(
remind(
&config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await,
Ok("No tasks for 'today'".to_string())
);
assert_eq!(
timebox(
&config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await,
Ok("No tasks for 'today'".to_string())
);
assert_eq!(
process(
&config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await,
Ok("No tasks for 'today'".to_string())
);
reminders_mock.assert();
tasks_mock.assert();
}
#[tokio::test]
async fn test_process_with_filter() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock2 = server
.mock("POST", "/api/v1/tasks/6Xqhv4cwxgjwG9w8/close")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTask.read().await)
.create_async()
.await;
let mock3 = server
.mock(
"GET",
"/api/v1/comments/?task_id=6Xqhv4cwxgjwG9w8&limit=200",
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::CommentsAllTypes.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(0)
.create()
.await
.expect("expected value or result, got None or Err");
let filter = String::from("today");
let sort = &SortOrder::Value;
let result = process(&config, Flag::Filter(filter), sort).await;
assert_eq!(result, Ok("Successfully processed 'today'".to_string()));
mock.assert();
mock2.assert();
mock3.assert();
}
#[tokio::test]
async fn test_process_with_project() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/?project_id=123&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock2 = server
.mock("POST", "/api/v1/tasks/6Xqhv4cwxgjwG9w8/close")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTask.read().await)
.create_async()
.await;
let mock3 = server
.mock(
"GET",
"/api/v1/comments/?task_id=6Xqhv4cwxgjwG9w8&limit=200",
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::CommentsAllTypes.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(0)
.create()
.await
.expect("expected value or result, got None or Err");
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let sort = &SortOrder::Value;
let result = process(&config, Flag::Project(project), sort).await;
assert_eq!(
result,
Ok(
"Successfully processed myproject\nhttps://app.todoist.com/app/project/123"
.to_string()
)
);
mock.assert();
mock2.assert();
mock3.assert();
}
#[tokio::test]
async fn test_process_returns_exited_when_quit_is_selected() {
let mut server = mockito::Server::new_async().await;
let tasks_mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let comments_mock = server
.mock(
"GET",
"/api/v1/comments/?task_id=6Xqhv4cwxgjwG9w8&limit=200",
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::CommentsAllTypes.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(6)
.create()
.await
.expect("config should be created");
let result = process(
&config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await;
assert_eq!(result, Ok("Exited".to_string()));
tasks_mock.assert();
comments_mock.assert();
}
#[tokio::test]
async fn test_process_handles_comment_fetch_errors() {
let mut server = mockito::Server::new_async().await;
let tasks_mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.expect(2)
.create_async()
.await;
let comments_mock = server
.mock(
"GET",
"/api/v1/comments/?task_id=6Xqhv4cwxgjwG9w8&limit=200",
)
.with_status(500)
.with_body("comment request failed")
.expect(2)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.mock_select(1)
.create()
.await
.expect("config should be created");
let skipped = process(
&config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await;
assert_eq!(skipped, Ok("Successfully processed 'today'".to_string()));
let quit_config = config
.mock_select(6)
.create()
.await
.expect("quit config should be created");
let exited = process(
&quit_config,
Flag::Filter("today".to_string()),
&SortOrder::Value,
)
.await;
assert_eq!(exited, Ok("Exited".to_string()));
tasks_mock.assert();
comments_mock.assert();
}
#[tokio::test]
async fn test_process_skips_cancelled_comment_fetch() {
let handle: tokio::task::JoinHandle<(Task, Result<Vec<Comment>, Error>)> =
tokio::spawn(std::future::pending());
handle.abort();
let cancelled = handle.await;
let config = test::fixtures::config().await;
let mut task_count = 1;
let outcome = process_task_with_comments(cancelled, &config, &mut task_count, false)
.await
.expect("cancelled comment fetch should be skipped");
assert!(matches!(outcome, ProcessTaskOutcome::Skip));
}
#[tokio::test]
async fn test_label() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock2 = server
.mock("POST", "/api/v1/tasks/6Xqhv4cwxgjwG9w8")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let config = test::fixtures::config().await.with_mock_url(server.url());
let config_dir = dirs::config_dir().expect("Could not find config directory");
let config_with_timezone = config
.with_timezone("US/Pacific")
.with_path(config_dir.join("test3"))
.with_mock_url(server.url())
.mock_select(0);
config_with_timezone
.clone()
.create()
.await
.expect("expected value or result, got None or Err");
let filter = String::from("today");
let labels = vec![String::from("thing")];
let sort = &SortOrder::Value;
assert_eq!(
label(&config_with_timezone, Flag::Filter(filter), &labels, sort).await,
Ok(String::from("Successfully labeled 'today'"))
);
mock.assert();
mock2.assert();
}
#[tokio::test]
async fn test_remind() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/reminders?limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"results":[],"next_cursor":null}"#)
.create_async()
.await;
let mock2 = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock3 = server
.mock("POST", "/api/v1/reminders")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{
"id": "abc",
"item_id": "6Xqhv4cwxgjwG9w8",
"notify_uid": "635166",
"type": "relative",
"is_deleted": false,
"minute_offset": 0,
"is_urgent": false,
"due": {
"date": "2026-01-18T17:00:00",
"timezone": null,
"string": "2026-01-18 17:00",
"lang": "en",
"is_recurring": false
}
}"#,
)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.with_mock_string("tomorrow");
let filter = String::from("today");
let sort = &SortOrder::Value;
let result = remind(&config, Flag::Filter(filter), sort).await;
assert_eq!(result, Ok(String::from("Successfully reminded 'today'")));
mock.assert();
mock2.assert();
mock3.assert();
}
#[tokio::test]
async fn test_remind_with_project() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/reminders?limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"results":[],"next_cursor":null}"#)
.create_async()
.await;
let mock2 = server
.mock("GET", "/api/v1/tasks/?project_id=123&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock3 = server
.mock("POST", "/api/v1/reminders")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{
"id": "abc",
"item_id": "6Xqhv4cwxgjwG9w8",
"notify_uid": "635166",
"type": "relative",
"is_deleted": false,
"minute_offset": 0,
"is_urgent": false,
"due": {
"date": "2026-01-18T17:00:00",
"timezone": null,
"string": "2026-01-18 17:00",
"lang": "en",
"is_recurring": false
}
}"#,
)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.with_mock_string("tomorrow");
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let sort = &SortOrder::Value;
let result = remind(&config, Flag::Project(project), sort).await;
assert_eq!(
result,
Ok(String::from(
"Successfully reminded myproject\nhttps://app.todoist.com/app/project/123"
))
);
mock.assert();
mock2.assert();
mock3.assert();
}
#[tokio::test]
async fn test_remind_with_project_completes_task() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/reminders?limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"results":[],"next_cursor":null}"#)
.create_async()
.await;
let mock2 = server
.mock("GET", "/api/v1/tasks/?project_id=123&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let mock3 = server
.mock("POST", "/api/v1/tasks/6Xqhv4cwxgjwG9w8/close")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTask.read().await)
.create_async()
.await;
let config = test::fixtures::config()
.await
.with_mock_url(server.url())
.with_mock_string("complete");
let binding = config
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.to_owned();
let sort = &SortOrder::Value;
let result = remind(&config, Flag::Project(project), sort).await;
assert_eq!(
result,
Ok(String::from(
"Successfully reminded myproject\nhttps://app.todoist.com/app/project/123"
))
);
mock.assert();
mock2.assert();
mock3.assert();
}
#[tokio::test]
async fn test_view() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/filter?query=today&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let config = test::fixtures::config().await.with_mock_url(server.url());
let mut config_with_timezone = config
.with_timezone("US/Pacific")
.with_mock_url(server.url());
let filter = String::from("today");
let sort = &SortOrder::Value;
let tasks = view(&mut config_with_timezone, Flag::Filter(filter), sort)
.await
.expect("expected value or result, got None or Err");
assert!(tasks.contains("Tasks for today"));
mock.assert();
}
#[tokio::test]
async fn test_view_with_project() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/v1/tasks/?project_id=123&limit=200")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(ResponseFromFile::TodayTasks.read().await)
.create_async()
.await;
let config = test::fixtures::config().await.with_mock_url(server.url());
let mut config_with_timezone = config
.with_timezone("US/Pacific")
.with_mock_url(server.url());
let binding = config_with_timezone
.projects()
.await
.expect("Failed to fetch projects asynchronously");
let project = binding
.first()
.expect("Expected at least one project in binding")
.clone();
let sort = &SortOrder::Value;
let tasks = view(&mut config_with_timezone, Flag::Project(project), sort)
.await
.expect("expected value or result, got None or Err");
assert!(tasks.contains("Tasks for"));
assert!(tasks.contains("- TEST\n"));
mock.assert();
}
}