diff --git a/Cargo.lock b/Cargo.lock index 44fd886..d8c3046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -794,6 +794,16 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.11.3" @@ -1219,7 +1229,7 @@ dependencies = [ "clap", "datamatrix", "enum-iterator", - "env_logger", + "env_logger 0.11.3", "futures-util", "itertools", "log", @@ -1227,6 +1237,8 @@ dependencies = [ "mime", "mime_guess", "printpdf", + "quickcheck", + "quickcheck_macros", "rust-embed", "serde", "serde_urlencoded", @@ -1664,6 +1676,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger 0.8.4", + "log", + "rand", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.36" diff --git a/Cargo.toml b/Cargo.toml index 39c2d2b..302e756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,5 +35,9 @@ thiserror = "1.0.61" time = { version = "0.3.36", features = ["parsing", "serde"] } uuid = { version = "1.9.0", features = ["serde", "v4"] } +[dev-dependencies] +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" + [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/src/database/item_states.rs b/src/database/item_states.rs index 456bbf8..51c5f3b 100644 --- a/src/database/item_states.rs +++ b/src/database/item_states.rs @@ -2,15 +2,20 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +#[cfg(test)] +use enum_iterator::Sequence; use maud::{html, Markup, Render}; +#[cfg(test)] +use quickcheck::{Arbitrary, Gen}; use sqlx::{query, PgPool}; use crate::frontend::templates::helpers::Colour; use super::item_events::ItemEvent; -#[derive(Clone, Copy, Debug, sqlx::Type)] +#[derive(Clone, Copy, Debug, PartialEq, sqlx::Type)] #[sqlx(rename_all = "snake_case", type_name = "item_state")] +#[cfg_attr(test, derive(Sequence))] pub enum ItemState { Borrowed, Inactive, @@ -18,6 +23,15 @@ pub enum ItemState { Owned, } +#[cfg(test)] +impl Arbitrary for ItemState { + fn arbitrary(g: &mut Gen) -> Self { + enum_iterator::all::() + .nth(usize::arbitrary(g) % enum_iterator::cardinality::()) + .unwrap() + } +} + impl ItemState { pub fn colour(&self) -> Colour { match self { diff --git a/src/database/items/add.rs b/src/database/items/add.rs index ef30ad0..05f34d7 100644 --- a/src/database/items/add.rs +++ b/src/database/items/add.rs @@ -65,3 +65,132 @@ impl ItemRepository { } } } + +#[cfg(test)] +mod tests { + use uuid::{uuid, Uuid}; + + use crate::database::{items::ItemAddForm, ItemRepository}; + use crate::test_utils::*; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn simple_success(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let added_item = repo + .add(ItemAddForm { + quantity: 1, + name: None, + parent: None, + class: uuid!("e993e21c-8558-49e7-a993-2a6a61c1d55c"), + original_packaging: None, + description: "descr".to_string(), + }) + .await?; + + assert_eq!(added_item.len(), 1); + + let added_item = added_item.first(); + + assert!( + sqlx::query_scalar( + "SELECT + items.name IS NULL + AND items.parent IS NULL + AND items.class = 'e993e21c-8558-49e7-a993-2a6a61c1d55c' + AND items.original_packaging IS NULL + AND items.description = 'descr' + FROM items + WHERE items.id = $1" + ) + .bind(added_item) + .fetch_one(&pool) + .await? + ); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn complex_success(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let added_items = repo + .add(ItemAddForm { + quantity: 7, + name: Some("Yeeeet".to_string()), + parent: Some(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")), + class: uuid!("e993e21c-8558-49e7-a993-2a6a61c1d55c"), + original_packaging: Some(uuid!("554b11ce-fecb-4020-981e-acabbf7b5913")), + description: "Lorem ipsum.".to_string(), + }) + .await?; + + assert_eq!(added_items.len(), 7); + + assert!( + sqlx::query_scalar( + "SELECT + items.name = 'Yeeeet' + AND items.parent = '4fc0f5f4-4dca-4c24-844d-1f464cb32afa' + AND items.class = 'e993e21c-8558-49e7-a993-2a6a61c1d55c' + AND items.original_packaging = '554b11ce-fecb-4020-981e-acabbf7b5913' + AND items.description = 'Lorem ipsum.' + FROM items + WHERE items.id = ANY ($1)" + ) + .bind(added_items) + .fetch_one(&pool) + .await? + ); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn invalid_references(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let item_count_before = item_count(&pool).await?; + + assert!(repo + .add(ItemAddForm { + quantity: 1, + name: None, + parent: Some(Uuid::new_v4()), + class: uuid!("e993e21c-8558-49e7-a993-2a6a61c1d55c"), + original_packaging: None, + description: "".to_string(), + }) + .await + .is_err()); + + assert!(repo + .add(ItemAddForm { + quantity: 1, + name: None, + parent: None, + class: Uuid::new_v4(), + original_packaging: None, + description: "".to_string(), + }) + .await + .is_err()); + + assert!(repo + .add(ItemAddForm { + quantity: 1, + name: None, + parent: None, + class: uuid!("e993e21c-8558-49e7-a993-2a6a61c1d55c"), + original_packaging: Some(Uuid::new_v4()), + description: "".to_string(), + }) + .await + .is_err()); + + assert_eq!(item_count(&pool).await?, item_count_before); + + Ok(()) + } +} diff --git a/src/database/items/datalist.rs b/src/database/items/datalist.rs index 765e5ae..f3406ba 100644 --- a/src/database/items/datalist.rs +++ b/src/database/items/datalist.rs @@ -29,3 +29,35 @@ impl ItemRepository { }) } } + +#[cfg(test)] +mod tests { + use maud::Render; + + use crate::database::{items::ItemName, ItemRepository}; + use crate::test_utils::*; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn datalist(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let dl = repo.datalist().await?; + + assert_eq!(dl.name, "items".to_string()); + assert_eq!(dl.link_prefix, Some("/item/".to_string())); + assert_eq!(dl.options.len(), item_count(&pool).await? as usize); + + // can’t compare Box + let option = dl + .options + .iter() + .find(|option| option.value == "554b11ce-fecb-4020-981e-acabbf7b5913") + .unwrap(); + assert_eq!( + option.text.render().into_string(), + ItemName::Item("Item 4".to_string()).render().into_string() + ); + + Ok(()) + } +} diff --git a/src/database/items/delete.rs b/src/database/items/delete.rs index 758a8d4..217b68b 100644 --- a/src/database/items/delete.rs +++ b/src/database/items/delete.rs @@ -16,3 +16,48 @@ impl ItemRepository { Ok(()) } } + +#[cfg(test)] +mod tests { + use uuid::{uuid, Uuid}; + + use crate::database::ItemRepository; + use crate::test_utils::*; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn success(pool: sqlx::PgPool) -> sqlx::Result<()> { + let item_count_before = item_count(&pool).await?; + + let repo = ItemRepository::new(pool.clone()); + repo.delete(uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617")) + .await?; + + assert_eq!(item_count(&pool).await?, item_count_before - 1); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn prevented_by_constraint(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + assert!(repo + .delete(uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26")) + .await + .is_err()); + + repo.delete(uuid!("554b11ce-fecb-4020-981e-acabbf7b5913")) + .await?; + repo.delete(uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26")) + .await?; + + Ok(()) + } + + #[sqlx::test] + async fn invalid_id(pool: sqlx::PgPool) { + let repo = ItemRepository::new(pool.clone()); + + assert!(repo.delete(Uuid::new_v4()).await.is_err()); + } +} diff --git a/src/database/items/edit.rs b/src/database/items/edit.rs index 061c3b9..b0f9a85 100644 --- a/src/database/items/edit.rs +++ b/src/database/items/edit.rs @@ -59,3 +59,89 @@ impl ItemRepository { Ok(()) } } + +#[cfg(test)] +mod tests { + use uuid::uuid; + + use crate::database::{items::ItemEditForm, ItemRepository}; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn prefill_form(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let form = repo + .edit_form(uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617")) + .await?; + + assert_eq!(form.name, Some("Item 2".to_string())); + assert_eq!( + form.parent, + Some(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")) + ); + assert_eq!(form.class, uuid!("8a979306-b4c6-4ef8-900d-68f64abb2975")); + assert_eq!( + form.original_packaging, + Some(uuid!("049298e2-73db-42fb-957d-a741655648b1")) + ); + assert_eq!(form.description, "Lorem ipsum 3".to_string()); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn edit(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + repo.edit( + uuid!("554b11ce-fecb-4020-981e-acabbf7b5913"), + &ItemEditForm { + name: Some("Totally new name".to_string()), + parent: Some(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")), + class: uuid!("04527cc8-2fbf-4a99-aa0a-361252c8f6d3"), + original_packaging: Some(uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26")), + description: "never seen before".to_string(), + }, + ) + .await?; + + assert!( + sqlx::query_scalar( + "SELECT + items.name = 'Totally new name' + AND items.parent = '4fc0f5f4-4dca-4c24-844d-1f464cb32afa' + AND items.class = '04527cc8-2fbf-4a99-aa0a-361252c8f6d3' + AND items.original_packaging = '3003e61f-0824-4625-9b72-eeb9f11a6a26' + AND items.description = 'never seen before' + FROM items + WHERE items.id = '554b11ce-fecb-4020-981e-acabbf7b5913'" + ) + .fetch_one(&pool) + .await? + ); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn invalid_parameters(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + // cycle + assert!(repo + .edit( + uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa"), + &ItemEditForm { + name: None, + parent: Some(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")), + class: uuid!("8a979306-b4c6-4ef8-900d-68f64abb2975"), + original_packaging: None, + description: "".to_string(), + }, + ) + .await + .is_err()); + + Ok(()) + } +} diff --git a/src/database/items/label.rs b/src/database/items/label.rs index 6802dd7..b8f934f 100644 --- a/src/database/items/label.rs +++ b/src/database/items/label.rs @@ -21,3 +21,42 @@ impl ItemRepository { .await } } + +#[cfg(test)] +mod tests { + use uuid::uuid; + + use crate::database::ItemRepository; + use crate::label::LabelPage; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn success_many(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let items = vec![ + uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa"), + uuid!("049298e2-73db-42fb-957d-a741655648b1"), + uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617"), + ]; + + let short_ids: Vec = + sqlx::query_scalar("SELECT short_id FROM items WHERE id = ANY ($1)") + .bind(&items) + .fetch_all(&pool) + .await?; + + assert_eq!( + repo.label_pages(&items).await?, + items + .into_iter() + .enumerate() + .map(|(idx, id)| LabelPage { + id: Some(id), + short_id: Some(format!("{:0>6}", short_ids[idx])) + }) + .collect::>() + ); + + Ok(()) + } +} diff --git a/src/database/items/list.rs b/src/database/items/list.rs index 499518e..e96d225 100644 --- a/src/database/items/list.rs +++ b/src/database/items/list.rs @@ -9,6 +9,7 @@ use super::ItemRepository; use super::{ItemName, ItemPreview}; use crate::database::item_states::ItemState; +#[derive(Debug, PartialEq)] pub struct ItemListEntry { pub id: Uuid, pub name: ItemName, @@ -93,3 +94,60 @@ impl ItemRepository { .await } } + +#[cfg(test)] +mod tests { + use uuid::uuid; + + use super::ItemListEntry; + use crate::database::{ + item_states::ItemState, + items::{ItemName, ItemPreview}, + ItemRepository, + }; + use crate::test_utils::*; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn list(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let list = repo.list().await?; + + assert_eq!(list.len(), item_count(&pool).await? as usize); + + assert!(list.contains(&ItemListEntry { + id: uuid!("554b11ce-fecb-4020-981e-acabbf7b5913"), + name: ItemName::Item("Item 4".to_string()), + class: uuid!("e993e21c-8558-49e7-a993-2a6a61c1d55c"), + class_name: "Class 1".to_string(), + // actual content is tested in test for ItemRepository::parents + parents: repo + .parents(uuid!("554b11ce-fecb-4020-981e-acabbf7b5913")) + .await?, + state: ItemState::Owned, + })); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn previews(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let previews = repo.previews().await?; + + assert_eq!(previews.len(), item_count(&pool).await? as usize); + + assert!(previews.contains(&ItemPreview::new( + uuid!("554b11ce-fecb-4020-981e-acabbf7b5913"), + ItemName::Item("Item 4".to_string()) + ))); + + assert!(previews.contains(&ItemPreview::new( + uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26"), + ItemName::Class("Class 1".to_string()) + ))); + + Ok(()) + } +} diff --git a/src/database/items/mod.rs b/src/database/items/mod.rs index 9f2ead0..c0bbe34 100644 --- a/src/database/items/mod.rs +++ b/src/database/items/mod.rs @@ -31,6 +31,7 @@ impl ItemRepository { } } +#[derive(Debug, PartialEq)] pub struct ItemPreview { pub id: Uuid, pub name: ItemName, @@ -124,3 +125,169 @@ impl ItemRepository { .await } } + +#[cfg(test)] +mod tests { + use quickcheck_macros::quickcheck; + use uuid::{uuid, Uuid}; + + use super::ItemPreview; + use crate::database::{item_states::ItemState, items::ItemName, ItemRepository}; + + #[quickcheck] + fn item_preview_new(id: u128, name: ItemName) { + let id = Uuid::from_u128(id); + assert_eq!( + ItemPreview::new(id, name.clone()), + ItemPreview { + id, + name, + state: None, + } + ); + } + + #[quickcheck] + fn item_preview_from_parts(id: u128, item_name: Option, class_name: String) { + let id = Uuid::from_u128(id); + assert_eq!( + ItemPreview::from_parts(id, item_name.as_ref(), &class_name), + ItemPreview { + id, + name: ItemName::new(item_name.as_ref(), &class_name), + state: None, + } + ); + } + + #[quickcheck] + fn item_preview_with_state(id: u128, name: ItemName, state: ItemState) { + let id = Uuid::from_u128(id); + assert_eq!( + ItemPreview::new(id, name.clone()).with_state(state), + ItemPreview { + id, + name, + state: Some(state), + } + ); + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn parents_multiple(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + assert_eq!( + repo.parents(uuid!("554b11ce-fecb-4020-981e-acabbf7b5913")) + .await?, + vec![ + ItemPreview::new( + uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa"), + ItemName::Item("Item 1".to_string()) + ), + ItemPreview::new( + uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26"), + ItemName::Class("Class 1".to_string()) + ), + ] + ); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn parents_none(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + assert!(repo + .parents(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")) + .await? + .is_empty()); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn children_multiple(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let children = repo + .children(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")) + .await?; + + let expected = [ + ItemPreview::new( + uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617"), + ItemName::Item("Item 2".to_string()), + ) + .with_state(ItemState::Owned), + ItemPreview::new( + uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26"), + ItemName::Class("Class 1".to_string()), + ) + .with_state(ItemState::Owned), + ]; + + assert_eq!(children.len(), expected.len()); + + // can’t use children == expected as order does not matter + assert!(children.iter().all(|child| expected.contains(child))); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn children_none(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + assert!(repo + .children(uuid!("554b11ce-fecb-4020-981e-acabbf7b5913")) + .await? + .is_empty()); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn original_packaging_of_multiple(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let original_packaging_of = repo + .original_packaging_of(uuid!("049298e2-73db-42fb-957d-a741655648b1")) + .await?; + + let expected = [ + ItemPreview::new( + uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617"), + ItemName::Item("Item 2".to_string()), + ) + .with_state(ItemState::Owned), + ItemPreview::new( + uuid!("4072791f-c5a0-41ac-9e63-2eb1d99b78de"), + ItemName::Item("Item 2 companion".to_string()), + ) + .with_state(ItemState::Owned), + ]; + + assert_eq!(original_packaging_of.len(), expected.len()); + + // can’t use original_packaging_of == expected as order does not matter + assert!(original_packaging_of + .iter() + .all(|child| expected.contains(child))); + + Ok(()) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn original_packaging_of_none(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + assert!(repo + .original_packaging_of(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")) + .await? + .is_empty()); + + Ok(()) + } +} diff --git a/src/database/items/name.rs b/src/database/items/name.rs index bfb385d..bc0a63b 100644 --- a/src/database/items/name.rs +++ b/src/database/items/name.rs @@ -2,12 +2,14 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +#[cfg(test)] +use quickcheck::{Arbitrary, Gen}; use sqlx::query; use uuid::Uuid; use super::ItemRepository; -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub enum ItemName { Item(String), Class(String), @@ -31,6 +33,18 @@ impl ItemName { } } +#[cfg(test)] +impl Arbitrary for ItemName { + fn arbitrary(g: &mut Gen) -> Self { + match u8::arbitrary(g) % 3 { + 0 => Self::Item(String::arbitrary(g)), + 1 => Self::Class(String::arbitrary(g)), + 2 => Self::None, + _ => unreachable!(), + } + } +} + impl ItemRepository { pub async fn name(&self, id: Uuid) -> sqlx::Result { query!( @@ -48,3 +62,46 @@ impl ItemRepository { .await } } + +#[cfg(test)] +mod tests { + use uuid::uuid; + + use super::ItemName; + use crate::database::ItemRepository; + use quickcheck_macros::quickcheck; + + #[quickcheck] + fn item_name_is_some(item_name: String, class_name: String) { + let name = ItemName::new(Some(&item_name), &class_name); + + assert_eq!(name, ItemName::Item(item_name)); + assert_eq!(name.clone().terse(), name); + } + + #[quickcheck] + fn item_name_is_none(class_name: String) { + let name = ItemName::new(None, &class_name); + + assert_eq!(name, ItemName::Class(class_name)); + assert_eq!(name.terse(), ItemName::None) + } + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn success(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + assert_eq!( + repo.name(uuid!("4fc0f5f4-4dca-4c24-844d-1f464cb32afa")) + .await?, + ItemName::Item("Item 1".to_string()) + ); + assert_eq!( + repo.name(uuid!("3003e61f-0824-4625-9b72-eeb9f11a6a26")) + .await?, + ItemName::Class("Class 1".to_string()) + ); + + Ok(()) + } +} diff --git a/src/database/items/show.rs b/src/database/items/show.rs index 87ccd43..6c2b8a0 100644 --- a/src/database/items/show.rs +++ b/src/database/items/show.rs @@ -5,9 +5,10 @@ use sqlx::query; use uuid::Uuid; -use super::{ItemRepository, ItemName, ItemPreview}; +use super::{ItemName, ItemPreview, ItemRepository}; use crate::database::item_states::ItemState; +#[derive(Debug, PartialEq)] pub struct ItemDetails { pub id: Uuid, pub short_id: i32, @@ -69,3 +70,50 @@ impl ItemRepository { .await } } + +#[cfg(test)] +mod tests { + use sqlx::query_scalar; + use uuid::uuid; + + use super::ItemDetails; + use crate::database::{ + item_states::ItemState, + items::{ItemName, ItemPreview}, + ItemRepository, + }; + + #[sqlx::test(fixtures(path = "../../../tests/fixtures", scripts("default")))] + async fn success(pool: sqlx::PgPool) -> sqlx::Result<()> { + let repo = ItemRepository::new(pool.clone()); + + let short_id: i32 = query_scalar( + "SELECT short_id FROM items WHERE id = '663f45e6-b11a-4197-8ce4-c784ac9ee617'", + ) + .fetch_one(&pool) + .await?; + + assert_eq!( + repo.details(uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617")) + .await?, + ItemDetails { + id: uuid!("663f45e6-b11a-4197-8ce4-c784ac9ee617"), + short_id, + name: ItemName::Item("Item 2".to_string()), + class: uuid!("8a979306-b4c6-4ef8-900d-68f64abb2975"), + class_name: "Subclass 1.1".to_string(), + original_packaging: Some( + ItemPreview::new( + uuid!("049298e2-73db-42fb-957d-a741655648b1"), + ItemName::Item("Original Packaging of Item 2".to_string()) + ) + .with_state(ItemState::Owned) + ), + description: "Lorem ipsum 3".to_string(), + state: ItemState::Owned, + } + ); + + Ok(()) + } +} diff --git a/src/label/mod.rs b/src/label/mod.rs index 7b22974..fbda57e 100644 --- a/src/label/mod.rs +++ b/src/label/mod.rs @@ -171,7 +171,7 @@ impl TextConfig { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct LabelPage { pub id: Option, pub short_id: Option, diff --git a/src/lib.rs b/src/lib.rs index 1869540..9ba3f8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,5 +7,7 @@ pub mod database; pub mod frontend; pub mod label; pub mod middleware; +#[cfg(test)] +mod test_utils; pub use config::Config; diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 0000000..e5b2a54 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use sqlx::query_scalar; + +pub async fn item_count(pool: &sqlx::PgPool) -> sqlx::Result { + query_scalar("SELECT count(id) FROM items") + .fetch_one(pool) + .await +} diff --git a/tests/fixtures/default.sql b/tests/fixtures/default.sql new file mode 100644 index 0000000..145df41 --- /dev/null +++ b/tests/fixtures/default.sql @@ -0,0 +1,34 @@ +-- SPDX-FileCopyrightText: 2024 Simon Bruder +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +INSERT INTO item_classes (id, name, parent, description) VALUES + ('e993e21c-8558-49e7-a993-2a6a61c1d55c', 'Class 1', NULL, 'Lorem ipsum 1'), + ('04527cc8-2fbf-4a99-aa0a-361252c8f6d3', 'Class 2', NULL, 'Lorem ipsum 2'), + ('9d760792-ddb0-47a0-bed1-c27dc41b285b', 'Class 3', NULL, 'Lorem ipsum 3'), + ('8a979306-b4c6-4ef8-900d-68f64abb2975', 'Subclass 1.1', 'e993e21c-8558-49e7-a993-2a6a61c1d55c', 'Lorem ipsum 4'), + ('042fe283-f645-401c-9079-3bd3ab1c3dc9', 'Subclass 1.2', 'e993e21c-8558-49e7-a993-2a6a61c1d55c', 'Lorem ipsum 5'), + ('01ad10ec-d3be-4346-9d44-4ebb7297a14d', 'Subclass 2.1', '04527cc8-2fbf-4a99-aa0a-361252c8f6d3', 'Lorem ipsum 6'); + +INSERT INTO items (id, name, parent, class, original_packaging, description) VALUES + ('4fc0f5f4-4dca-4c24-844d-1f464cb32afa', 'Item 1', NULL, 'e993e21c-8558-49e7-a993-2a6a61c1d55c', NULL, 'Lorem ipsum 1'), + ('049298e2-73db-42fb-957d-a741655648b1', 'Original Packaging of Item 2', NULL, '01ad10ec-d3be-4346-9d44-4ebb7297a14d', NULL, 'Lorem ipsum 2'), + ('663f45e6-b11a-4197-8ce4-c784ac9ee617', 'Item 2', '4fc0f5f4-4dca-4c24-844d-1f464cb32afa', '8a979306-b4c6-4ef8-900d-68f64abb2975', '049298e2-73db-42fb-957d-a741655648b1', 'Lorem ipsum 3'), + ('4072791f-c5a0-41ac-9e63-2eb1d99b78de', 'Item 2 companion', '049298e2-73db-42fb-957d-a741655648b1', '042fe283-f645-401c-9079-3bd3ab1c3dc9', '049298e2-73db-42fb-957d-a741655648b1', 'Lorem ipsum 10'), + ('3003e61f-0824-4625-9b72-eeb9f11a6a26', NULL, '4fc0f5f4-4dca-4c24-844d-1f464cb32afa', 'e993e21c-8558-49e7-a993-2a6a61c1d55c', NULL, 'Lorem ipsum 4'), + ('554b11ce-fecb-4020-981e-acabbf7b5913', 'Item 4', '3003e61f-0824-4625-9b72-eeb9f11a6a26', 'e993e21c-8558-49e7-a993-2a6a61c1d55c', NULL, 'Lorem ipsum 5'), + ('b9fce434-faa4-4242-bd06-9d3589fa41e7', 'Borrowed Item', NULL, '9d760792-ddb0-47a0-bed1-c27dc41b285b', NULL, 'Lorem ipsum 6'), + ('2683d77f-2d9c-4a5c-b87f-6e1a99c69db0', 'Loaned Item', NULL, '9d760792-ddb0-47a0-bed1-c27dc41b285b', NULL, 'Lorem ipsum 7'), + ('5ca9ed99-2e70-4723-9ae4-0bb5ab274366', 'Inactive Item', NULL, '9d760792-ddb0-47a0-bed1-c27dc41b285b', NULL, 'Lorem ipsum 8'), + ('2da2643d-c759-48ab-8cdf-e4d46c8ecc69', 'Owned Item (bought)', NULL, '9d760792-ddb0-47a0-bed1-c27dc41b285b', NULL, 'Lorem ipsum 9'); + +DELETE FROM item_events WHERE item = ANY (ARRAY[ + 'b9fce434-faa4-4242-bd06-9d3589fa41e7', + '2da2643d-c759-48ab-8cdf-e4d46c8ecc69' +]::uuid[]); + +INSERT INTO item_events (item, event, description) VALUES + ('b9fce434-faa4-4242-bd06-9d3589fa41e7', 'borrow', 'from Jane Person'), + ('2683d77f-2d9c-4a5c-b87f-6e1a99c69db0', 'loan', 'to Joe Person'), + ('5ca9ed99-2e70-4723-9ae4-0bb5ab274366', 'gift', 'to Jude Person'), + ('2da2643d-c759-48ab-8cdf-e4d46c8ecc69', 'buy', 'from garage sale');