From 8ad6e090f9a9807d697fe434fbe6b7c4459ba546 Mon Sep 17 00:00:00 2001 From: ToBinio Date: Wed, 6 May 2026 15:21:04 +0200 Subject: [PATCH] add todo time handling --- api/migration/src/lib.rs | 2 + .../src/m20260506_113244_add_todo_time.rs | 62 +++ api/src/entities/mod.rs | 1 + api/src/entities/prelude.rs | 1 + api/src/entities/time.rs | 39 ++ api/src/entities/todo.rs | 8 + api/src/routes/categories.rs | 16 +- api/src/routes/tags.rs | 16 +- api/src/routes/todo.rs | 16 +- api/src/services/todos.rs | 138 ++++++- api/tests/todos/add.rs | 16 +- api/tests/todos/update.rs | 354 +++++++++++++++++- 12 files changed, 611 insertions(+), 58 deletions(-) create mode 100644 api/migration/src/m20260506_113244_add_todo_time.rs create mode 100644 api/src/entities/time.rs diff --git a/api/migration/src/lib.rs b/api/migration/src/lib.rs index 9e2bbc8..7b34a78 100644 --- a/api/migration/src/lib.rs +++ b/api/migration/src/lib.rs @@ -5,6 +5,7 @@ mod m20260503_125344_add_category_table; mod m20260504_125311_add_basic_todo_table; mod m20260505_071439_add_todo_order; mod m20260505_085735_add_todo_checks; +mod m20260506_113244_add_todo_time; pub struct Migrator; @@ -17,6 +18,7 @@ impl MigratorTrait for Migrator { Box::new(m20260504_125311_add_basic_todo_table::Migration), Box::new(m20260505_071439_add_todo_order::Migration), Box::new(m20260505_085735_add_todo_checks::Migration), + Box::new(m20260506_113244_add_todo_time::Migration), ] } } diff --git a/api/migration/src/m20260506_113244_add_todo_time.rs b/api/migration/src/m20260506_113244_add_todo_time.rs new file mode 100644 index 0000000..083f93e --- /dev/null +++ b/api/migration/src/m20260506_113244_add_todo_time.rs @@ -0,0 +1,62 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +use crate::m20260504_125311_add_basic_todo_table::Todo; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Time::Table) + .if_not_exists() + .col(pk_uuid(Time::TodoId)) + .col(enumeration( + Time::Format, + Time::Format, + ["point", "range", "recurring"], + )) + .col(date_null(Time::DateStart)) + .col(date_null(Time::DateEnd)) + .col(date_time_null(Time::DateTime)) + .col(enumeration_null( + Time::RecurrenceMode, + Time::RecurrenceMode, + ["daily", "weekly"], + )) + .col(integer_null(Time::NumberStart)) + .col(integer_null(Time::NumberEnd)) + .foreign_key( + ForeignKey::create() + .from(Time::Table, Time::TodoId) + .to(Todo::Table, Todo::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Time::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum Time { + Table, + TodoId, + Format, + DateStart, + DateEnd, + DateTime, + RecurrenceMode, + NumberStart, + NumberEnd, +} diff --git a/api/src/entities/mod.rs b/api/src/entities/mod.rs index 9d008ec..45c43bf 100644 --- a/api/src/entities/mod.rs +++ b/api/src/entities/mod.rs @@ -5,5 +5,6 @@ pub mod prelude; pub mod category; pub mod check; pub mod tag; +pub mod time; pub mod todo; pub mod todo_tag; diff --git a/api/src/entities/prelude.rs b/api/src/entities/prelude.rs index 1439d0c..bb7af81 100644 --- a/api/src/entities/prelude.rs +++ b/api/src/entities/prelude.rs @@ -3,5 +3,6 @@ pub use super::category::Entity as Category; pub use super::check::Entity as Check; pub use super::tag::Entity as Tag; +pub use super::time::Entity as Time; pub use super::todo::Entity as Todo; pub use super::todo_tag::Entity as TodoTag; diff --git a/api/src/entities/time.rs b/api/src/entities/time.rs new file mode 100644 index 0000000..46a2e90 --- /dev/null +++ b/api/src/entities/time.rs @@ -0,0 +1,39 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "time")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub todo_id: Uuid, + #[sea_orm(column_type = "custom(\"enum_text\")")] + pub format: String, + pub date_start: Option, + pub date_end: Option, + pub date_time: Option, + #[sea_orm(column_type = "custom(\"enum_text\")", nullable)] + pub recurrence_mode: Option, + pub number_start: Option, + pub number_end: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::todo::Entity", + from = "Column::TodoId", + to = "super::todo::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Todo, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Todo.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/api/src/entities/todo.rs b/api/src/entities/todo.rs index 3891582..bcf5b65 100644 --- a/api/src/entities/todo.rs +++ b/api/src/entities/todo.rs @@ -26,6 +26,8 @@ pub enum Relation { Category, #[sea_orm(has_many = "super::check::Entity")] Check, + #[sea_orm(has_one = "super::time::Entity")] + Time, #[sea_orm(has_many = "super::todo_tag::Entity")] TodoTag, } @@ -42,6 +44,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Time.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::TodoTag.def() diff --git a/api/src/routes/categories.rs b/api/src/routes/categories.rs index 132a772..be96d7d 100644 --- a/api/src/routes/categories.rs +++ b/api/src/routes/categories.rs @@ -40,16 +40,12 @@ async fn add_category( })?; let new_category = match existing_category { - Some(existing_category) => { - CategoryService::update(&user, &state.db_connection, category) - .await - .map_err(|e| { - warn!("failed to add category: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add category") - })?; - - existing_category - } + Some(_) => CategoryService::update(&user, &state.db_connection, category) + .await + .map_err(|e| { + warn!("failed to add category: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "failed to add category") + })?, None => CategoryService::create(&user, &state.db_connection, category) .await .map_err(|e| { diff --git a/api/src/routes/tags.rs b/api/src/routes/tags.rs index 2d53fd0..8332676 100644 --- a/api/src/routes/tags.rs +++ b/api/src/routes/tags.rs @@ -40,16 +40,12 @@ async fn add_tag( })?; let new_tag = match existing_tag { - Some(existing_tag) => { - TagService::update(&user, &state.db_connection, tag) - .await - .map_err(|e| { - warn!("failed to add tag: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add tag") - })?; - - existing_tag - } + Some(_) => TagService::update(&user, &state.db_connection, tag) + .await + .map_err(|e| { + warn!("failed to add tag: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "failed to add tag") + })?, None => TagService::create(&user, &state.db_connection, tag) .await .map_err(|e| { diff --git a/api/src/routes/todo.rs b/api/src/routes/todo.rs index 123abe2..82dd568 100644 --- a/api/src/routes/todo.rs +++ b/api/src/routes/todo.rs @@ -71,16 +71,12 @@ async fn add_todo( })?; let new_todo = match existing_todo { - Some(existing_todo) => { - TodoService::update(&user, &state.db_connection, todo.data) - .await - .map_err(|e| { - warn!("failed to add todo: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "failed to add todo") - })?; - - existing_todo - } + Some(_) => TodoService::update(&user, &state.db_connection, todo.data) + .await + .map_err(|e| { + warn!("failed to add todo: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "failed to add todo") + })?, None => TodoService::create( &user, &state.db_connection, diff --git a/api/src/services/todos.rs b/api/src/services/todos.rs index f7692b3..20e92ca 100644 --- a/api/src/services/todos.rs +++ b/api/src/services/todos.rs @@ -5,14 +5,15 @@ use migration::Expr; use sea_orm::ActiveValue::{NotSet, Set}; use sea_orm::{ ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, IntoActiveModel, ModelTrait, - QueryFilter, QueryOrder, + PaginatorTrait, QueryFilter, QueryOrder, }; use serde::Deserialize; -use types::Todo as TodoModel; +use tracing::warn; +use types::{Time as TimeModel, TimeRange, TimeRecurring, Todo as TodoModel}; use uuid::Uuid; use crate::auth::User; -use crate::entities::{check, prelude::*, todo, todo_tag}; +use crate::entities::{check, prelude::*, time, todo, todo_tag}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -47,7 +48,7 @@ impl TodoService { }); let mut checks: HashMap> = Check::find() - .filter(check::Column::TodoId.is_in(todo_ids)) + .filter(check::Column::TodoId.is_in(todo_ids.clone())) .all(db) .await? .into_iter() @@ -56,17 +57,32 @@ impl TodoService { acc }); + let mut times: HashMap> = Time::find() + .filter(time::Column::TodoId.is_in(todo_ids)) + .all(db) + .await? + .into_iter() + .fold(HashMap::new(), |mut acc, time| { + acc.entry(time.todo_id).or_default().push(time.into()); + acc + }); + let result: Vec = todos .into_iter() .map(|todo| { let tags = tags.remove(&todo.id).unwrap_or_default(); let checks = checks.remove(&todo.id).unwrap_or_default(); + let mut times = times.remove(&todo.id).unwrap_or_default(); + + if times.len() > 1 { + warn!("Todo with multiple times found - todo-uuid: {}", todo.id); + } TodoModel { uuid: todo.id, title: todo.title, note: todo.note, - time: None, + time: times.pop(), tags, category: None, checks, @@ -93,12 +109,13 @@ impl TodoService { let tags = todo.find_related(TodoTag).all(db).await?; let checks = todo.find_related(Check).all(db).await?; + let time = todo.find_related(Time).one(db).await?; Ok(Some(TodoModel { uuid: todo.id, title: todo.title.to_string(), note: todo.note.to_string(), - time: None, + time: time.map(|time| time.into()), tags: tags.iter().map(|tag| tag.tag_id).collect(), category: todo.category_id, checks: checks.iter().map(|check| check.date).collect(), @@ -127,7 +144,7 @@ impl TodoService { .unwrap_or(0); let todo_model = Todo::insert(todo::ActiveModel { - id: Set(todo.uuid.clone()), + id: Set(todo.uuid), owner_id: Set(user.uuid), title: Set(todo.title), note: Set(todo.note), @@ -137,14 +154,6 @@ impl TodoService { .exec_with_returning(db) .await?; - TodoTag::insert_many(todo.tags.iter().map(|tag| todo_tag::ActiveModel { - tag_id: Set(tag.clone()), - todo_id: Set(todo.uuid.clone()), - })) - .on_empty_do_nothing() - .exec(db) - .await?; - if let Some(reference) = reference { let target = Todo::find_by_id(reference) .filter(todo::Column::OwnerId.eq(user.uuid)) @@ -161,6 +170,22 @@ impl TodoService { } } + if !todo.tags.is_empty() { + TodoTag::insert_many(todo.tags.iter().map(|tag| todo_tag::ActiveModel { + tag_id: Set(*tag), + todo_id: Set(todo.uuid), + })) + .exec(db) + .await?; + } + + if let Some(time) = todo.time { + let mut time_model = time::ActiveModel::from(time); + time_model.todo_id = Set(todo.uuid); + + Time::insert(time_model).exec(db).await?; + } + Self::get_by_id(user, db, todo.uuid) .await .map(|todo| todo.unwrap()) @@ -212,6 +237,22 @@ impl TodoService { .await?; } + let has_time = Time::find_by_id(todo.uuid).exists(db).await?; + if let Some(time) = todo.time { + let mut time_model: time::ActiveModel = time.into(); + time_model.todo_id = Set(todo.uuid); + + if has_time { + Time::update(time_model).exec(db).await?; + } else { + Time::insert(time_model).exec(db).await?; + } + } else { + if has_time { + Time::delete_by_id(todo.uuid).exec(db).await?; + } + } + Self::get_by_id(user, db, updated.id) .await .map(|todo| todo.unwrap()) @@ -325,3 +366,70 @@ impl TodoService { Ok(()) } } + +impl From for time::ActiveModel { + fn from(value: TimeModel) -> Self { + match value { + types::Time::Range { inner } => time::ActiveModel { + format: Set("range".to_string()), + date_start: Set(Some(inner.start)), + date_end: Set(Some(inner.end)), + ..Default::default() + }, + types::Time::Recurring { inner } => match inner { + types::TimeRecurring::Daily {} => time::ActiveModel { + format: Set("recurring".to_string()), + recurrence_mode: Set(Some("daily".to_string())), + ..Default::default() + }, + types::TimeRecurring::Weekly { start, end } => time::ActiveModel { + format: Set("recurring".to_string()), + recurrence_mode: Set(Some("weekly".to_string())), + number_start: Set(Some(start as i32)), + number_end: Set(Some(end as i32)), + ..Default::default() + }, + }, + types::Time::Point { inner } => time::ActiveModel { + format: Set("point".to_string()), + date_time: Set(Some(inner.time.naive_utc())), + ..Default::default() + }, + } + } +} + +impl From for TimeModel { + fn from(value: time::Model) -> Self { + match value.format.as_str() { + "range" => types::Time::Range { + inner: TimeRange { + start: value.date_start.unwrap(), + end: value.date_end.unwrap(), + }, + }, + "recurring" => match value.recurrence_mode.unwrap().as_str() { + "weekly" => types::Time::Recurring { + inner: TimeRecurring::Weekly { + start: value.number_start.unwrap() as u64, + end: value.number_end.unwrap() as u64, + }, + }, + "daily" => types::Time::Recurring { + inner: TimeRecurring::Daily {}, + }, + mode => { + panic!("unknown recurrence mode: {}", mode) + } + }, + "point" => types::Time::Point { + inner: types::TimePoint { + time: value.date_time.unwrap().and_utc(), + }, + }, + format => { + panic!("unknown time format: {}", format) + } + } + } +} diff --git a/api/tests/todos/add.rs b/api/tests/todos/add.rs index 3bdde9f..3f7c43a 100644 --- a/api/tests/todos/add.rs +++ b/api/tests/todos/add.rs @@ -47,7 +47,7 @@ async fn add_todo() { #[tokio::test] #[serial] -async fn add_todo_with_point_time() { +async fn add_todo_with_time_point() { let client = get_client(); clear_db(&client).await; @@ -71,8 +71,6 @@ async fn add_todo_with_point_time() { checks: vec![], }; - println!("{}", serde_json::to_string(&todo).unwrap()); - let response = client .add_todo(json!( { @@ -102,7 +100,7 @@ async fn add_todo_with_point_time() { #[tokio::test] #[serial] -async fn add_todo_with_point_range() { +async fn add_todo_with_time_range() { let client = get_client(); clear_db(&client).await; @@ -122,8 +120,6 @@ async fn add_todo_with_point_range() { checks: vec![], }; - println!("{}", serde_json::to_string(&todo).unwrap()); - let response = client .add_todo(json!( { @@ -154,7 +150,7 @@ async fn add_todo_with_point_range() { #[tokio::test] #[serial] -async fn add_todo_with_point_recurring_weekly() { +async fn add_todo_with_time_recurring_weekly() { let client = get_client(); clear_db(&client).await; @@ -171,8 +167,6 @@ async fn add_todo_with_point_recurring_weekly() { checks: vec![], }; - println!("{}", serde_json::to_string(&todo).unwrap()); - let response = client .add_todo(json!( { @@ -204,7 +198,7 @@ async fn add_todo_with_point_recurring_weekly() { #[tokio::test] #[serial] -async fn add_todo_with_point_recurring_daily() { +async fn add_todo_with_time_recurring_daily() { let client = get_client(); clear_db(&client).await; @@ -238,8 +232,6 @@ async fn add_todo_with_point_recurring_daily() { })) .await; - assert_eq!(response.status().as_str(), "200"); - let json = response.json::().await.unwrap(); assert_eq!(json, todo); diff --git a/api/tests/todos/update.rs b/api/tests/todos/update.rs index 9b79d78..25d9aa5 100644 --- a/api/tests/todos/update.rs +++ b/api/tests/todos/update.rs @@ -1,11 +1,57 @@ use crate::common::{client::get_client, db::clear_db}; +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; use serde_json::json; use serial_test::serial; +use types::{TimePoint, TimeRange, TimeRecurring, Todo}; use uuid::Uuid; #[tokio::test] #[serial] -async fn update_todos() { +async fn update_todo() { + let client = get_client(); + clear_db(&client).await; + + let todo_uuid = Uuid::new_v4(); + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "tags": [], + "checks": [], + } + } + )) + .await; + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "updated Todo", + "note": "updated Note", + "tags": [], + "checks": [], + } + } + )) + .await; + + let todos = client.get_todos_json().await; + + assert_eq!(todos.len(), 1); + assert_eq!(todos[0].uuid, todo_uuid); + assert_eq!(todos[0].title, "updated Todo"); + assert_eq!(todos[0].note, "updated Note"); +} + +#[tokio::test] +#[serial] +async fn update_todo_with_labels() { let client = get_client(); clear_db(&client).await; @@ -93,6 +139,312 @@ async fn update_todos() { assert_eq!(todos[0].tags.len(), 0); } +#[tokio::test] +#[serial] +async fn update_todo_with_time_point() { + let client = get_client(); + clear_db(&client).await; + + let todo_uuid = Uuid::new_v4(); + let todo = Todo { + uuid: todo_uuid, + title: "Test Todo".to_string(), + note: "Test Note".to_string(), + time: Some(types::Time::Point { + inner: TimePoint { + time: DateTime::from_naive_utc_and_offset( + NaiveDate::from_ymd_opt(1970, 1, 1) + .unwrap() + .and_time(NaiveTime::from_hms_opt(20, 5, 2).unwrap()), + Utc, + ), + }, + }), + tags: vec![], + category: None, + checks: vec![], + }; + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "tags": [], + "checks": [], + } + })) + .await; + + let response = client + .add_todo(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "time": { + "type": "point", + "time": "1970-01-01T20:05:02.0Z" + }, + "tags": [], + "checks": [], + } + })) + .await; + + assert_eq!(response.status().as_str(), "200"); + + let json = response.json::().await.unwrap(); + assert_eq!(json, todo); + + let todos = client.get_todos_json().await; + assert_eq!(todos.len(), 1); + assert_eq!(todos.first().unwrap(), &todo); +} + +#[tokio::test] +#[serial] +async fn update_todo_with_time_range() { + let client = get_client(); + clear_db(&client).await; + + let todo_uuid = Uuid::new_v4(); + let todo = Todo { + uuid: todo_uuid, + title: "Test Todo".to_string(), + note: "Test Note".to_string(), + time: Some(types::Time::Range { + inner: TimeRange { + start: NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + end: NaiveDate::from_ymd_opt(1970, 2, 1).unwrap(), + }, + }), + tags: vec![], + category: None, + checks: vec![], + }; + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "tags": [], + "checks": [], + } + })) + .await; + + let response = client + .add_todo(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "time": { + "type": "range", + "start": "1970-01-01", + "end": "1970-02-01" + }, + "tags": [], + "checks": [], + } + })) + .await; + + assert_eq!(response.status().as_str(), "200"); + + let json = response.json::().await.unwrap(); + assert_eq!(json, todo); + + let todos = client.get_todos_json().await; + assert_eq!(todos.len(), 1); + assert_eq!(todos.first().unwrap(), &todo); +} + +#[tokio::test] +#[serial] +async fn update_todo_with_time_recurring_weekly() { + let client = get_client(); + clear_db(&client).await; + + let todo_uuid = Uuid::new_v4(); + let todo = Todo { + uuid: todo_uuid, + title: "Test Todo".to_string(), + note: "Test Note".to_string(), + time: Some(types::Time::Recurring { + inner: TimeRecurring::Weekly { start: 2, end: 4 }, + }), + tags: vec![], + category: None, + checks: vec![], + }; + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "tags": [], + "checks": [], + } + })) + .await; + + let response = client + .add_todo(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "time": { + "type": "recurring", + "mode": "weekly", + "start": 2, + "end": 4 + }, + "tags": [], + "checks": [], + } + })) + .await; + + assert_eq!(response.status().as_str(), "200"); + + let json = response.json::().await.unwrap(); + assert_eq!(json, todo); + + let todos = client.get_todos_json().await; + assert_eq!(todos.len(), 1); + assert_eq!(todos.first().unwrap(), &todo); +} + +#[tokio::test] +#[serial] +async fn update_todo_with_time_recurring_daily() { + let client = get_client(); + clear_db(&client).await; + + let todo_uuid = Uuid::new_v4(); + let todo = Todo { + uuid: todo_uuid, + title: "Test Todo".to_string(), + note: "Test Note".to_string(), + time: Some(types::Time::Recurring { + inner: TimeRecurring::Daily {}, + }), + tags: vec![], + category: None, + checks: vec![], + }; + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "tags": [], + "checks": [], + } + })) + .await; + + let response = client + .add_todo(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "time": { + "type": "recurring", + "mode": "daily", + }, + "tags": [], + "checks": [], + } + })) + .await; + + assert_eq!(response.status().as_str(), "200"); + + let json = response.json::().await.unwrap(); + assert_eq!(json, todo); + + let todos = client.get_todos_json().await; + assert_eq!(todos.len(), 1); + assert_eq!(todos.first().unwrap(), &todo); +} + +#[tokio::test] +#[serial] +async fn update_todo_with_remove_time() { + let client = get_client(); + clear_db(&client).await; + + let todo_uuid = Uuid::new_v4(); + let todo = Todo { + uuid: todo_uuid, + title: "Test Todo".to_string(), + note: "Test Note".to_string(), + time: None, + tags: vec![], + category: None, + checks: vec![], + }; + + client + .add_todo_json(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "time": { + "type": "recurring", + "mode": "daily", + }, + "tags": [], + "checks": [], + } + })) + .await; + + let response = client + .add_todo(json!( + { + "data": { + "uuid": todo_uuid, + "title": "Test Todo", + "note": "Test Note", + "tags": [], + "checks": [], + } + })) + .await; + + assert_eq!(response.status().as_str(), "200"); + + let json = response.json::().await.unwrap(); + assert_eq!(json, todo); + + let todos = client.get_todos_json().await; + assert_eq!(todos.len(), 1); + assert_eq!(todos.first().unwrap(), &todo); +} + #[tokio::test] #[serial] async fn update_invalid_todos() { -- 2.51.2