WIP: Add unit tests
All checks were successful
/ build (push) Successful in 11s

TODO:

- add fixture to avoid code duplication
This commit is contained in:
Simon Bruder 2024-07-25 22:43:58 +02:00
parent 716ac1a698
commit f355a4319b
Signed by: simon
GPG key ID: 347FF8699CDA0776
10 changed files with 421 additions and 16 deletions

View file

@ -20,7 +20,7 @@ jobs:
git config --unset "http.${GITHUB_SERVER_URL}/.extraHeader" git config --unset "http.${GITHUB_SERVER_URL}/.extraHeader"
git lfs install --local git lfs install --local
git lfs pull git lfs pull
- name: Build - name: Build and test
run: nix build -L .#li7y .#li7y-oci run: nix build -L .#li7y .#li7y-oci
- name: Push OCI image - name: Push OCI image
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'

1
.gitignore vendored
View file

@ -3,5 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
/target /target
/.tarpaulin_target
result* result*
.pre-commit-config.yaml .pre-commit-config.yaml

16
.tarpaulin.toml Normal file
View file

@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
[default]
# Tarpaulin uses custom options
# that are incompatible with the default options.
# This sets a different target directory,
# so the files from tarpaulin do not interfere with the regular outputs.
target-dir = ".tarpaulin_target"
# Do not recompile everything on every run
skip-clean = true
[report]
out = ["Html"]
output-dir = "target/tarpaulin"

View file

@ -74,31 +74,74 @@
}; };
}; };
packages = rec { packages =
li7y = naersk'.buildPackage { let
src = self; # naersk does not easily allow overrideAttrs
}; commonNaerskConfigurarion = {
default = li7y; src = self;
li7y-oci = pkgs.dockerTools.buildLayeredImage { checkInputs = with pkgs; [
name = "li7y"; postgresql
tag = "latest"; postgresqlTestHook
];
contents = [ doCheck = true;
li7y
];
config = { # tests need to be able to create and drop databases
Cmd = [ "${li7y}/bin/li7y" ]; postgresqlTestUserOptions = "LOGIN SUPERUSER";
postgresqlTestSetupPost = ''
export DATABASE_URL="postgres://''${PGUSER}/''${PGDATABASE}?port=5432&host=''${PGHOST}"
'';
# Otherwise SQLx tries to infer the databse schema from an empty database
# (as it can only run the migrations once the test binary is built).
# Also, this enforces that the full query cache is included in the repository.
SQLX_OFFLINE = true;
};
in
rec {
li7y = naersk'.buildPackage commonNaerskConfigurarion;
default = li7y;
li7y-tarpaulin = naersk'.buildPackage (commonNaerskConfigurarion // {
name = "li7y-tarpaulin";
checkInputs = commonNaerskConfigurarion.checkInputs ++ (with pkgs; [
cargo-tarpaulin
]);
dontBuild = true;
singleStep = true; # tarpaulin uses different options anyway
cargoTestCommands = _: [ "cargo tarpaulin" ];
postInstall = ''
rm -r $out
cp -r target/tarpaulin $out
'';
});
li7y-oci = pkgs.dockerTools.buildLayeredImage {
name = "li7y";
tag = "latest";
contents = [
li7y
];
config = {
Cmd = [ "${li7y}/bin/li7y" ];
};
}; };
}; };
};
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
buildInputs = [ buildInputs = [
rustPackageDev rustPackageDev
] ++ (with pkgs; [ ] ++ (with pkgs; [
cargo-deny cargo-deny
cargo-tarpaulin
cargo-watch cargo-watch
clippy clippy
graphviz graphviz

View file

@ -65,3 +65,147 @@ impl ItemRepository {
} }
} }
} }
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::database::{items::ItemAddForm, ItemRepository};
use crate::test_utils::*;
#[sqlx::test]
async fn simple_success(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let class = create_class(pool.clone()).await?;
let added_item = repo
.add(ItemAddForm {
quantity: 1,
name: None,
parent: None,
class,
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 = $2
AND items.original_packaging IS NULL
AND items.description = 'descr'
FROM items
WHERE items.id = $1"
)
.bind(added_item)
.bind(class)
.fetch_one(&pool)
.await?
);
Ok(())
}
#[sqlx::test]
async fn complex_success(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let class = create_class(pool.clone()).await?;
let parent = create_item(pool.clone()).await?;
let original_packaging = create_item(pool.clone()).await?;
let added_items = repo
.add(ItemAddForm {
quantity: 7,
name: Some("Yeeeet".to_string()),
parent: Some(parent),
class,
original_packaging: Some(original_packaging),
description: "Lorem ipsum.".to_string(),
})
.await?;
assert_eq!(added_items.len(), 7);
assert!(
sqlx::query_scalar(
"SELECT
items.name = 'Yeeeet'
AND items.parent = $2
AND items.class = $3
AND items.original_packaging = $4
AND items.description = 'Lorem ipsum.'
FROM items
WHERE items.id = ANY ($1)"
)
.bind(added_items)
.bind(parent)
.bind(class)
.bind(original_packaging)
.fetch_one(&pool)
.await?
);
Ok(())
}
#[sqlx::test]
async fn invalid_references(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let class = create_class(pool.clone()).await?;
assert!(repo
.add(ItemAddForm {
quantity: 1,
name: None,
parent: Some(Uuid::new_v4()),
class,
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,
original_packaging: Some(Uuid::new_v4()),
description: "".to_string(),
})
.await
.is_err());
let item_count: i64 = sqlx::query_scalar("SELECT count(id) FROM items")
.fetch_one(&pool)
.await?;
assert_eq!(item_count, 0);
Ok(())
}
}

View file

@ -16,3 +16,64 @@ impl ItemRepository {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::database::ItemRepository;
use crate::test_utils::*;
#[sqlx::test]
async fn success(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let item = create_item(pool.clone()).await?;
let item_count: i64 = sqlx::query_scalar("SELECT count(id) FROM items")
.fetch_one(&pool)
.await?;
assert_eq!(item_count, 1);
repo.delete(item).await?;
let item_count: i64 = sqlx::query_scalar("SELECT count(id) FROM items")
.fetch_one(&pool)
.await?;
assert_eq!(item_count, 0);
Ok(())
}
#[sqlx::test]
async fn prevented_by_constraint(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let item = create_item(pool.clone()).await?;
let child = create_item(pool.clone()).await?;
sqlx::query("UPDATE items SET parent = $2 WHERE id = $1")
.bind(child)
.bind(item)
.execute(&pool)
.await?;
assert!(repo.delete(item).await.is_err());
assert!(repo.delete(child).await.is_ok());
assert!(repo.delete(item).await.is_ok());
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());
}
}

View file

@ -59,3 +59,104 @@ impl ItemRepository {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::database::{items::ItemEditForm, ItemRepository};
use crate::test_utils::*;
#[sqlx::test]
async fn prefill_form(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let item = create_item(pool.clone()).await?;
let form = repo.edit_form(item).await?;
let item_data: (Option<String>, Option<Uuid>, Uuid, Option<Uuid>, String) = sqlx::query_as(
"SELECT name, parent, class, original_packaging, description
FROM items
WHERE id = $1",
)
.bind(item)
.fetch_one(&pool)
.await?;
assert_eq!(form.name, item_data.0);
assert_eq!(form.parent, item_data.1);
assert_eq!(form.class, item_data.2);
assert_eq!(form.original_packaging, item_data.3);
assert_eq!(form.description, item_data.4);
Ok(())
}
#[sqlx::test]
async fn edit(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let item = create_item(pool.clone()).await?;
let parent = create_item(pool.clone()).await?;
let original_packaging = create_item(pool.clone()).await?;
let class = create_class(pool.clone()).await?;
repo.edit(
item,
&ItemEditForm {
name: Some("Totally new name".to_string()),
parent: Some(parent),
class,
original_packaging: Some(original_packaging),
description: "never seen before".to_string(),
},
)
.await?;
assert!(
sqlx::query_scalar(
"SELECT
items.name = 'Totally new name'
AND items.parent = $2
AND items.class = $3
AND items.original_packaging = $4
AND items.description = 'never seen before'
FROM items
WHERE items.id = $1"
)
.bind(item)
.bind(parent)
.bind(class)
.bind(original_packaging)
.fetch_one(&pool)
.await?
);
Ok(())
}
#[sqlx::test]
async fn invalid_parameters(pool: sqlx::PgPool) -> sqlx::Result<()> {
let repo = ItemRepository::new(pool.clone());
let item = create_item(pool.clone()).await?;
let class = create_class(pool.clone()).await?;
assert!(repo
.edit(
item,
&ItemEditForm {
name: None,
parent: Some(item),
class,
original_packaging: None,
description: "".to_string(),
},
)
.await
.is_err());
Ok(())
}
}

View file

@ -5,7 +5,7 @@
use sqlx::query; use sqlx::query;
use uuid::Uuid; use uuid::Uuid;
use super::{ItemRepository, ItemName, ItemPreview}; use super::{ItemName, ItemPreview, ItemRepository};
use crate::database::item_states::ItemState; use crate::database::item_states::ItemState;
pub struct ItemDetails { pub struct ItemDetails {

View file

@ -7,5 +7,7 @@ pub mod database;
pub mod frontend; pub mod frontend;
pub mod label; pub mod label;
pub mod middleware; pub mod middleware;
#[cfg(test)]
mod test_utils;
pub use config::Config; pub use config::Config;

37
src/test_utils.rs Normal file
View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use crate::database::{
item_classes::ItemClassAddForm, items::ItemAddForm, ItemClassRepository, ItemRepository,
};
pub async fn create_class(pool: sqlx::PgPool) -> sqlx::Result<Uuid> {
let class_repo = ItemClassRepository::new(pool);
class_repo
.add(ItemClassAddForm {
name: "Foo".to_string(),
parent: None,
description: "".to_string(),
})
.await
}
pub async fn create_item(pool: sqlx::PgPool) -> sqlx::Result<Uuid> {
let item_repo = ItemRepository::new(pool.clone());
item_repo
.add(ItemAddForm {
quantity: 1,
name: Some("Baz".to_string()),
parent: None,
class: create_class(pool.clone()).await?,
original_packaging: None,
description: "descr".to_string(),
})
.await
.map(|ids| *ids.first().unwrap())
}