diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c9985f..1df931a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,3 +6,4 @@ repos: - id: cargo-check args: ['--tests'] - id: clippy + args: ['--tests', '--', '-D', 'warnings'] diff --git a/Cargo.lock b/Cargo.lock index bb3b11e..9cfb223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,7 +443,7 @@ dependencies = [ [[package]] name = "contextswitch-types" version = "0.1.0" -source = "git+https://github.com/dax/contextswitch-types.git#d99bd6e6aebece04a41bdf62f00eaafcb73640ea" +source = "git+https://github.com/dax/contextswitch-types.git#974890d5f59efd257dd77faa44ec5106efcb1330" dependencies = [ "chrono", "http", diff --git a/src/contextswitch/api.rs b/src/contextswitch/api.rs index 86b3899..cbaf621 100644 --- a/src/contextswitch/api.rs +++ b/src/contextswitch/api.rs @@ -23,12 +23,8 @@ impl std::fmt::Debug for ContextswitchError { #[derive(thiserror::Error)] pub enum ContextswitchError { - #[error("Invalid Contextswitch data: {data}")] - InvalidDataError { - #[source] - source: serde_json::Error, - data: String, - }, + #[error("Invalid Contextswitch data")] + InvalidDataError(#[from] serde_json::Error), #[error(transparent)] UnexpectedError(#[from] anyhow::Error), } @@ -48,5 +44,13 @@ pub async fn add_task(add_args: Vec<&str>) -> Result { let taskwarrior_task = taskwarrior::add_task(add_args) .await .map_err(|e| ContextswitchError::UnexpectedError(e.into()))?; - Ok((&taskwarrior_task).into()) + Ok(taskwarrior_task.into()) +} + +#[tracing::instrument(level = "debug")] +pub async fn update_task(task_to_update: Task) -> Result { + let taskwarrior_task = taskwarrior::update_task(task_to_update.try_into()?) + .await + .map_err(|e| ContextswitchError::UnexpectedError(e.into()))?; + Ok(taskwarrior_task.into()) } diff --git a/src/contextswitch/taskwarrior.rs b/src/contextswitch/taskwarrior.rs index 624c3d0..b8d9dcc 100644 --- a/src/contextswitch/taskwarrior.rs +++ b/src/contextswitch/taskwarrior.rs @@ -15,6 +15,125 @@ use tokio::sync::Mutex; use tracing::{debug, warn}; use uuid::Uuid; +use super::ContextswitchError; + +#[tracing::instrument(level = "debug")] +pub fn list_tasks(filters: Vec<&str>) -> Result, TaskwarriorError> { + let args = [filters, vec!["export"]].concat(); + let export_output = Command::new("task") + .args(args) + .output() + .map_err(TaskwarriorError::ExecutionError)?; + + let output = + String::from_utf8(export_output.stdout).context("Failed to read Taskwarrior output")?; + + let tasks: Vec = serde_json::from_str(&output) + .map_err(|e| TaskwarriorError::OutputParsingError { source: e, output })?; + + Ok(tasks) +} + +#[tracing::instrument(level = "debug")] +pub fn get_task_by_local_id( + id: &TaskwarriorTaskLocalId, +) -> Result, TaskwarriorError> { + let mut tasks: Vec = list_tasks(vec![&id.to_string()])?; + if tasks.len() > 1 { + return Err(TaskwarriorError::UnexpectedError(anyhow!( + "Found more than 1 task when searching for task with local ID {}", + id + ))); + } + + Ok(tasks.pop()) +} + +#[tracing::instrument(level = "debug")] +pub fn get_task_by_id( + uuid: &TaskwarriorTaskId, +) -> Result, TaskwarriorError> { + let mut tasks: Vec = list_tasks(vec![&uuid.to_string()])?; + if tasks.len() > 1 { + return Err(TaskwarriorError::UnexpectedError(anyhow!( + "Found more than 1 task when searching for task with UUID {}", + uuid + ))); + } + + Ok(tasks.pop()) +} + +lazy_static! { + static ref RE: Regex = Regex::new(r"Modified 1 task.").unwrap(); + static ref TW_WRITE_LOCK: Mutex = Mutex::new(0); +} + +#[tracing::instrument(level = "debug")] +pub async fn add_task(add_args: Vec<&str>) -> Result { + lazy_static! { + static ref RE: Regex = Regex::new(r"Created task (?P\d+).").unwrap(); + } + let _lock = TW_WRITE_LOCK.lock().await; + + let args = [vec!["add"], add_args].concat(); + let add_output = Command::new("task") + .args(args) + .output() + .map_err(TaskwarriorError::ExecutionError)?; + let output = + String::from_utf8(add_output.stdout).context("Failed to read Taskwarrior output")?; + let task_id_capture = RE + .captures(&output) + .ok_or_else(|| anyhow!("Cannot extract task ID from: {}", &output))?; + let task_id_str = task_id_capture + .name("id") + .ok_or_else(|| anyhow!("Cannot extract task ID value from: {}", &output))? + .as_str(); + + let task_id = TaskwarriorTaskLocalId( + task_id_str + .parse::() + .context("Cannot parse task ID value")?, + ); + + let task = get_task_by_local_id(&task_id)?; + task.ok_or_else(|| { + TaskwarriorError::UnexpectedError(anyhow!( + "Newly created task with ID {} was not found", + task_id + )) + }) +} + +#[tracing::instrument(level = "debug")] +pub async fn update_task(action: TaskwarriorAction) -> Result { + lazy_static! { + static ref RE: Regex = Regex::new(r"Modified 1 task.").unwrap(); + } + + let _lock = TW_WRITE_LOCK.lock().await; + let args = [ + vec![action.uuid.to_string(), "mod".to_string()], + action.args, + ] + .concat(); + Command::new("task") + .args(args) + .output() + .map_err(TaskwarriorError::ExecutionError)?; + + let updated_task = get_task_by_id(&action.uuid)?; + updated_task.ok_or_else(|| { + TaskwarriorError::UnexpectedError(anyhow!( + "Updated task with UUID {} was not found", + action.uuid + )) + }) +} + +// Types +// TaskwarriorTask #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)] pub struct TaskwarriorTaskLocalId(pub u64); @@ -33,12 +152,18 @@ impl fmt::Display for TaskwarriorTaskId { } } -impl From<&TaskwarriorTaskId> for TaskId { - fn from(task: &TaskwarriorTaskId) -> Self { +impl From for TaskId { + fn from(task: TaskwarriorTaskId) -> Self { TaskId(task.0) } } +impl From for TaskwarriorTaskId { + fn from(task: TaskId) -> Self { + TaskwarriorTaskId(task.0) + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct TaskwarriorTask { pub uuid: TaskwarriorTaskId, @@ -75,7 +200,7 @@ pub struct TaskwarriorTask { )] pub wait: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent: Option, + pub parent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -88,6 +213,12 @@ pub struct TaskwarriorTask { pub contextswitch: Option, } +impl From for Task { + fn from(task: TaskwarriorTask) -> Self { + (&task).into() + } +} + impl From<&TaskwarriorTask> for Task { fn from(task: &TaskwarriorTask) -> Self { let cs_data = @@ -105,7 +236,7 @@ impl From<&TaskwarriorTask> for Task { }); Task { - id: (&task.uuid).into(), + id: task.uuid.clone().into(), entry: task.entry, modified: task.modified, status: task.status, @@ -115,7 +246,7 @@ impl From<&TaskwarriorTask> for Task { start: task.start, end: task.end, wait: task.wait, - parent: task.parent, + parent: task.parent.clone().map(|id| id.into()), project: task.project.clone(), priority: task.priority, recur: task.recur, @@ -125,6 +256,108 @@ impl From<&TaskwarriorTask> for Task { } } +// TaskwarriorAction +#[derive(Debug)] +pub struct TaskwarriorAction { + pub uuid: TaskwarriorTaskId, + pub args: Vec, +} + +fn to_arg(arg: &str) -> impl Fn(String) -> String + '_ { + move |value: String| format!("{}:{}", arg, value) +} + +fn format_date(date: DateTime) -> String { + date.format("%Y-%m-%dT%H:%M:%SZ").to_string() +} + +impl TryFrom for TaskwarriorAction { + type Error = ContextswitchError; + + fn try_from(task: Task) -> Result { + (&task).try_into() + } +} + +fn format_json(data_opt: &Option) -> Result, ContextswitchError> +where + T: Sized + Serialize, +{ + data_opt + .as_ref() + .map(|data| serde_json::to_string(data).map_err(ContextswitchError::InvalidDataError)) + .transpose() +} + +impl TryFrom<&Task> for TaskwarriorAction { + type Error = ContextswitchError; + + fn try_from(task: &Task) -> Result { + let args = vec![task.description.clone()]; + let tags_args = task + .tags + .clone() + .map(|tags| { + tags.iter() + .map(|tag| format!("+{}", tag)) // TODO remove tags + .collect::>() + }) + .unwrap_or_else(Vec::new); + let opt_args = [ + task.due + .map(format_date) + .or_else(|| Some("".to_string())) + .map(to_arg("due")), + task.start + .map(format_date) + .or_else(|| Some("".to_string())) + .map(to_arg("start")), + task.end + .map(format_date) + .or_else(|| Some("".to_string())) + .map(to_arg("end")), + task.wait + .map(format_date) + .or_else(|| Some("".to_string())) + .map(to_arg("wait")), + task.parent + .as_ref() + .map(|id| id.to_string()) + .or_else(|| Some("".to_string())) + .map(to_arg("parent")), + task.project + .clone() + .or_else(|| Some("".to_string())) + .map(to_arg("project")), + task.priority + .map(|priority| priority.to_string()) + .or_else(|| Some("".to_string())) + .map(to_arg("priority")), + task.recur + .map(|recur| recur.to_string()) + .or_else(|| Some("".to_string())) + .map(to_arg("recur")), + format_json(&task.contextswitch)? + .or_else(|| Some("".to_string())) + .map(to_arg("contextswitch")), + ]; + + Ok(TaskwarriorAction { + uuid: task.id.clone().into(), + args: [ + args, + tags_args, + opt_args + .iter() + .filter_map(|arg| arg.clone()) + .collect::>(), + ] + .concat(), + }) + } +} + +// Errors #[derive(thiserror::Error, Debug)] pub enum TaskwarriorError { #[error("Error while executing Taskwarrior")] @@ -139,6 +372,7 @@ pub enum TaskwarriorError { UnexpectedError(#[from] anyhow::Error), } +// Taskwarrior config functions fn write_default_config(data_location: &str) -> String { let mut taskrc = Ini::new(); taskrc.setstr("default", "data.location", Some(data_location)); @@ -192,77 +426,6 @@ pub fn load_config(settings: &TaskwarriorSettings) -> String { } } -#[tracing::instrument(level = "debug")] -pub fn list_tasks(filters: Vec<&str>) -> Result, TaskwarriorError> { - let args = [filters, vec!["export"]].concat(); - let export_output = Command::new("task") - .args(args) - .output() - .map_err(TaskwarriorError::ExecutionError)?; - - let output = - String::from_utf8(export_output.stdout).context("Failed to read Taskwarrior output")?; - - let tasks: Vec = serde_json::from_str(&output) - .map_err(|e| TaskwarriorError::OutputParsingError { source: e, output })?; - - Ok(tasks) -} - -#[tracing::instrument(level = "debug")] -pub fn get_task_by_local_id( - id: &TaskwarriorTaskLocalId, -) -> Result, TaskwarriorError> { - let mut tasks: Vec = list_tasks(vec![&id.to_string()])?; - if tasks.len() > 1 { - return Err(TaskwarriorError::UnexpectedError(anyhow!( - "Found more than 1 task when searching for task with local ID {}", - id - ))); - } - - Ok(tasks.pop()) -} - -#[tracing::instrument(level = "debug")] -pub async fn add_task(add_args: Vec<&str>) -> Result { - lazy_static! { - static ref RE: Regex = Regex::new(r"Created task (?P\d+).").unwrap(); - static ref LOCK: Mutex = Mutex::new(0); - } - let _lock = LOCK.lock().await; - - let mut args = vec!["add"]; - args.extend(add_args); - let add_output = Command::new("task") - .args(args) - .output() - .map_err(TaskwarriorError::ExecutionError)?; - let output = - String::from_utf8(add_output.stdout).context("Failed to read Taskwarrior output")?; - let task_id_capture = RE - .captures(&output) - .ok_or_else(|| anyhow!("Cannot extract task ID from: {}", &output))?; - let task_id_str = task_id_capture - .name("id") - .ok_or_else(|| anyhow!("Cannot extract task ID value from: {}", &output))? - .as_str(); - - let task_id = TaskwarriorTaskLocalId( - task_id_str - .parse::() - .context("Cannot parse task ID value")?, - ); - - let task = get_task_by_local_id(&task_id)?; - task.ok_or_else(|| { - TaskwarriorError::UnexpectedError(anyhow!( - "Newly created task with ID {} was not found", - task_id - )) - }) -} - #[cfg(test)] mod tests { @@ -281,14 +444,14 @@ mod tests { entry: Utc.ymd(2022, 1, 1).and_hms(1, 0, 0), modified: Utc.ymd(2022, 1, 1).and_hms(1, 0, 1), status: contextswitch_types::Status::Pending, - description: String::from("simple task"), + description: "simple task".to_string(), urgency: 0.5, due: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 2)), start: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 3)), end: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 4)), wait: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 5)), - parent: Some(Uuid::new_v4()), - project: Some(String::from("simple project")), + parent: Some(TaskwarriorTaskId(Uuid::new_v4())), + project: Some("simple project".to_string()), priority: Some(contextswitch_types::Priority::H), recur: Some(contextswitch_types::Recurrence::Daily), tags: Some(vec!["tag1".to_string(), "tag2".to_string()]), @@ -308,7 +471,10 @@ mod tests { assert_eq!(tw_task.start, cs_task.start); assert_eq!(tw_task.end, cs_task.end); assert_eq!(tw_task.wait, cs_task.wait); - assert_eq!(tw_task.parent, cs_task.parent); + assert_eq!( + tw_task.parent.map(|id| id.to_string()), + cs_task.parent.map(|id| id.to_string()) + ); assert_eq!(tw_task.project, cs_task.project); assert_eq!(tw_task.priority, cs_task.priority); assert_eq!(tw_task.recur, cs_task.recur); @@ -333,7 +499,7 @@ mod tests { entry: Utc.ymd(2022, 1, 1).and_hms(1, 0, 0), modified: Utc.ymd(2022, 1, 1).and_hms(1, 0, 1), status: contextswitch_types::Status::Pending, - description: String::from("simple task"), + description: "simple task".to_string(), urgency: 0.5, due: None, start: None, @@ -344,7 +510,7 @@ mod tests { priority: None, recur: None, tags: None, - contextswitch: Some(cs_data.to_string()), + contextswitch: Some(cs_data), }; let cs_task: Task = (&tw_task).into(); @@ -358,4 +524,110 @@ mod tests { } } } + + mod from_contextswitch_task_to_taskwarrior_action { + use super::super::*; + use chrono::TimeZone; + use contextswitch_types::{Bookmark, Priority, Recurrence}; + use http::Uri; + + #[test] + fn test_successful_convertion() { + let task = Task { + id: TaskId(Uuid::new_v4()), + entry: Utc.ymd(2022, 1, 1).and_hms(1, 0, 0), + modified: Utc.ymd(2022, 1, 1).and_hms(1, 0, 1), + status: contextswitch_types::Status::Pending, + description: "simple task".to_string(), + urgency: 0.5, + due: None, + start: None, + end: None, + wait: None, + parent: None, + project: None, + priority: None, + recur: None, + tags: None, + contextswitch: None, + }; + let action: TaskwarriorAction = (&task) + .try_into() + .expect("Failed to convert Task into TaskwarriorAction"); + + assert_eq!(task.id.0, action.uuid.0); + assert_eq!( + vec![ + task.description, + "due:".to_string(), + "start:".to_string(), + "end:".to_string(), + "wait:".to_string(), + "parent:".to_string(), + "project:".to_string(), + "priority:".to_string(), + "recur:".to_string(), + "contextswitch:".to_string(), + ], + action.args + ); + } + + #[test] + fn test_successful_full_convertion() { + let task = Task { + id: TaskId(Uuid::new_v4()), + entry: Utc.ymd(2022, 1, 1).and_hms(1, 0, 0), + modified: Utc.ymd(2022, 1, 1).and_hms(1, 0, 1), + status: contextswitch_types::Status::Pending, + description: "simple task".to_string(), + urgency: 0.5, + due: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 2)), + start: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 3)), + end: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 4)), + wait: Some(Utc.ymd(2022, 1, 1).and_hms(1, 0, 5)), + parent: Some(TaskId(Uuid::new_v4())), + project: Some("myproject".to_string()), + priority: Some(Priority::H), + recur: Some(Recurrence::Monthly), + tags: Some(vec!["tag1".to_string(), "tag2".to_string()]), + contextswitch: Some(ContextswitchData { + bookmarks: vec![ + Bookmark { + uri: "https://www.example.com/path".parse::().unwrap(), + content: None, + }, + Bookmark { + uri: "https://www.example.com/path2".parse::().unwrap(), + content: None, + }, + ], + }), + }; + let action: TaskwarriorAction = (&task) + .try_into() + .expect("Failed to convert Task into TaskwarriorAction"); + + assert_eq!(task.id.0, action.uuid.0); + assert_eq!( + vec![ + task.description, + "+tag1".to_string(), + "+tag2".to_string(), + "due:2022-01-01T01:00:02Z".to_string(), + "start:2022-01-01T01:00:03Z".to_string(), + "end:2022-01-01T01:00:04Z".to_string(), + "wait:2022-01-01T01:00:05Z".to_string(), + format!("parent:{}", task.parent.unwrap()), + "project:myproject".to_string(), + "priority:H".to_string(), + "recur:monthly".to_string(), + String::from( + r#"contextswitch:{"bookmarks":[{"uri":"https://www.example.com/path"},{"uri":"https://www.example.com/path2"}]}"# + ) + ], + action.args + ); + } + } } diff --git a/src/lib.rs b/src/lib.rs index a3ebb5c..b18e044 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub fn run(listener: TcpListener) -> Result { .route("/ping", web::get().to(routes::ping)) .route("/tasks", web::get().to(routes::list_tasks)) .route("/tasks", web::post().to(routes::add_task)) + .route("/tasks/{task_id}", web::put().to(routes::update_task)) .route( "/tasks", web::method(http::Method::OPTIONS).to(routes::option_task), diff --git a/src/routes/tasks.rs b/src/routes/tasks.rs index ad728f7..fdf4fb7 100644 --- a/src/routes/tasks.rs +++ b/src/routes/tasks.rs @@ -1,7 +1,7 @@ use crate::contextswitch; use actix_web::{http::StatusCode, web, HttpResponse, ResponseError}; use anyhow::Context; -use contextswitch_types::{NewTask, Task}; +use contextswitch_types::{NewTask, Task, TaskId}; use serde::Deserialize; #[derive(Deserialize)] @@ -37,17 +37,33 @@ pub async fn list_tasks( .body(serde_json::to_string(&tasks).context("Cannot serialize Contextswitch task")?)) } -#[tracing::instrument(level = "debug", skip_all, fields(definition = %task.definition))] +#[tracing::instrument(level = "debug", skip_all, fields(definition = %new_task.definition))] pub async fn add_task( - task: web::Json, + new_task: web::Json, ) -> Result { - let task: Task = contextswitch::add_task(task.definition.split(' ').collect()).await?; + let task: Task = contextswitch::add_task(new_task.definition.split(' ').collect()).await?; Ok(HttpResponse::Ok() .content_type("application/json") .body(serde_json::to_string(&task).context("Cannot serialize Contextswitch task")?)) } +#[tracing::instrument(level = "debug", skip_all)] +pub async fn update_task( + path: web::Path, + task: web::Json, +) -> Result { + let task_to_update = task.into_inner(); + if path.into_inner() != task_to_update.id { + return Ok(HttpResponse::BadRequest().finish()); + } + let task_updated: Task = contextswitch::update_task(task_to_update).await?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(serde_json::to_string(&task_updated).context("Cannot serialize Contextswitch task")?)) +} + #[tracing::instrument(level = "debug")] pub fn option_task() -> HttpResponse { HttpResponse::Ok().finish() diff --git a/tests/api/main.rs b/tests/api/main.rs index 7de4230..cd2d67b 100644 --- a/tests/api/main.rs +++ b/tests/api/main.rs @@ -1,3 +1,3 @@ mod health_check; mod helpers; -mod task; +mod tasks; diff --git a/tests/api/task.rs b/tests/api/task.rs deleted file mode 100644 index 20c26b5..0000000 --- a/tests/api/task.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::helpers::app_address; -use contextswitch_api::contextswitch; -use contextswitch_types::{Bookmark, ContextswitchData, NewTask, Task, TaskId}; -use http::uri::Uri; -use rstest::*; -use uuid::Uuid; - -#[rstest] -#[tokio::test] -async fn list_tasks(app_address: &str) { - let task = contextswitch::add_task(vec![ - "test", - "list_tasks", - "contextswitch:'{\"bookmarks\":[{\"uri\":\"https://example.com/path?filter=1\"}]}'", - ]) - .await - .unwrap(); - - let tasks: Vec = reqwest::Client::new() - .get(&format!("{}/tasks?filter={}", &app_address, task.id)) - .send() - .await - .expect("Failed to execute request") - .json() - .await - .expect("Cannot parse JSON result"); - - assert_eq!(tasks.len(), 1); - assert_eq!(tasks[0].description, "test list_tasks"); - let cs_data = tasks[0].contextswitch.as_ref().unwrap(); - assert_eq!(cs_data.bookmarks.len(), 1); - assert_eq!(cs_data.bookmarks[0].content, None); - assert_eq!( - cs_data.bookmarks[0].uri, - "https://example.com/path?filter=1".parse::().unwrap() - ); -} - -#[rstest] -#[tokio::test] -async fn list_tasks_with_unknown_contextswitch_data(app_address: &str) { - let task = contextswitch::add_task(vec![ - "test", - "list_tasks_with_unknown_contextswitch_data", - "contextswitch:'{\"unknown\": 1}'", - ]) - .await - .unwrap(); - - let tasks: Vec = reqwest::Client::new() - .get(&format!("{}/tasks?filter={}", &app_address, task.id)) - .send() - .await - .expect("Failed to execute request") - .json() - .await - .expect("Cannot parse JSON result"); - - assert_eq!(tasks.len(), 1); - assert_eq!( - tasks[0].description, - "test list_tasks_with_unknown_contextswitch_data" - ); - assert!(tasks[0].contextswitch.is_none()); -} - -#[rstest] -#[tokio::test] -async fn list_tasks_with_invalid_contextswitch_data(app_address: &str) { - let task = contextswitch::add_task(vec![ - "test", - "list_tasks_with_invalid_contextswitch_data", - "contextswitch:'}'", - ]) - .await - .unwrap(); - - let tasks: Vec = reqwest::Client::new() - .get(&format!("{}/tasks?filter={}", &app_address, task.id)) - .send() - .await - .expect("Failed to execute request") - .json() - .await - .expect("Cannot parse JSON result"); - - assert_eq!(tasks.len(), 1); - assert_eq!( - tasks[0].description, - "test list_tasks_with_invalid_contextswitch_data" - ); - assert!(tasks[0].contextswitch.is_none()); -} - -#[rstest] -#[tokio::test] -async fn add_task(app_address: &str) { - let response: serde_json::Value = reqwest::Client::new() - .post(&format!("{}/tasks", &app_address)) - .json(&NewTask { - definition: - "test add_task contextswitch:{\"bookmarks\":[{\"uri\":\"https://example.com/path?filter=1\"}]}" - .to_string(), - }) - .send() - .await - .expect("Failed to execute request") - .json() - .await - .expect("Cannot parse JSON result"); - let new_task_id = TaskId(Uuid::parse_str(response["id"].as_str().unwrap()).unwrap()); - let tasks = contextswitch::list_tasks(vec![&new_task_id.to_string()]).unwrap(); - - assert_eq!(tasks.len(), 1); - assert_eq!(tasks[0].id, new_task_id); - assert_eq!(tasks[0].description, "test add_task"); - assert_eq!( - tasks[0].contextswitch.as_ref().unwrap(), - &ContextswitchData { - bookmarks: vec![Bookmark { - uri: "https://example.com/path?filter=1".parse::().unwrap(), - content: None - }] - } - ); -} diff --git a/tests/api/tasks.rs b/tests/api/tasks.rs new file mode 100644 index 0000000..d4143d8 --- /dev/null +++ b/tests/api/tasks.rs @@ -0,0 +1,181 @@ +use crate::helpers::app_address; +use contextswitch_api::contextswitch; +use contextswitch_types::{Bookmark, ContextswitchData, NewTask, Task}; +use http::uri::Uri; +use rstest::*; + +mod list_tasks { + use super::*; + + #[rstest] + #[tokio::test] + async fn list_tasks(app_address: &str) { + let task = contextswitch::add_task(vec![ + "test", + "list_tasks", + "contextswitch:'{\"bookmarks\":[{\"uri\":\"https://example.com/path?filter=1\"}]}'", + ]) + .await + .unwrap(); + + let tasks: Vec = reqwest::Client::new() + .get(&format!("{}/tasks?filter={}", &app_address, task.id)) + .send() + .await + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); + + assert_eq!(tasks.len(), 1); + assert_eq!(tasks[0].description, "test list_tasks"); + let cs_data = tasks[0].contextswitch.as_ref().unwrap(); + assert_eq!(cs_data.bookmarks.len(), 1); + assert_eq!(cs_data.bookmarks[0].content, None); + assert_eq!( + cs_data.bookmarks[0].uri, + "https://example.com/path?filter=1".parse::().unwrap() + ); + } + + #[rstest] + #[tokio::test] + async fn list_tasks_with_unknown_contextswitch_data(app_address: &str) { + let task = contextswitch::add_task(vec![ + "test", + "list_tasks_with_unknown_contextswitch_data", + "contextswitch:'{\"unknown\": 1}'", + ]) + .await + .unwrap(); + + let tasks: Vec = reqwest::Client::new() + .get(&format!("{}/tasks?filter={}", &app_address, task.id)) + .send() + .await + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); + + assert_eq!(tasks.len(), 1); + assert_eq!( + tasks[0].description, + "test list_tasks_with_unknown_contextswitch_data" + ); + assert!(tasks[0].contextswitch.is_none()); + } + + #[rstest] + #[tokio::test] + async fn list_tasks_with_invalid_contextswitch_data(app_address: &str) { + let task = contextswitch::add_task(vec![ + "test", + "list_tasks_with_invalid_contextswitch_data", + "contextswitch:'}'", + ]) + .await + .unwrap(); + + let tasks: Vec = reqwest::Client::new() + .get(&format!("{}/tasks?filter={}", &app_address, task.id)) + .send() + .await + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); + + assert_eq!(tasks.len(), 1); + assert_eq!( + tasks[0].description, + "test list_tasks_with_invalid_contextswitch_data" + ); + assert!(tasks[0].contextswitch.is_none()); + } +} + +mod add_task { + use super::*; + + #[rstest] + #[tokio::test] + async fn add_task(app_address: &str) { + let task: Task = reqwest::Client::new() + .post(&format!("{}/tasks", &app_address)) + .json(&NewTask { + definition: + "test add_task contextswitch:{\"bookmarks\":[{\"uri\":\"https://example.com/path?filter=1\"}]}" + .to_string(), + }) + .send() + .await + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); + + assert_eq!(task.description, "test add_task"); + assert_eq!( + task.contextswitch.as_ref().unwrap(), + &ContextswitchData { + bookmarks: vec![Bookmark { + uri: "https://example.com/path?filter=1".parse::().unwrap(), + content: None + }] + } + ); + } +} + +mod update_task { + use super::*; + + #[rstest] + #[tokio::test] + async fn update_task(app_address: &str) { + let mut task = contextswitch::add_task(vec![ + "test", + "update_task", + "contextswitch:'{\"bookmarks\":[{\"uri\":\"https://example.com/path?filter=1\"}]}'", + ]) + .await + .unwrap(); + + task.description = "updated task description".to_string(); + let cs_data = task.contextswitch.as_mut().unwrap(); + cs_data.bookmarks.push(Bookmark { + uri: "https://example.com/path2".parse::().unwrap(), + content: None, + }); + + let updated_task: Task = reqwest::Client::new() + .put(&format!("{}/tasks/{}", &app_address, task.id)) + .json(&task) + .send() + .await + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); + + assert_eq!(updated_task.description, "updated task description"); + assert_eq!( + updated_task.contextswitch.as_ref().unwrap(), + &ContextswitchData { + bookmarks: vec![ + Bookmark { + uri: "https://example.com/path?filter=1".parse::().unwrap(), + content: None + }, + Bookmark { + uri: "https://example.com/path2".parse::().unwrap(), + content: None + } + ] + } + ); + } + + // TODO : test incoherent task id +}