TODO: - add fixture to avoid code duplication
This commit is contained in:
parent
716ac1a698
commit
f355a4319b
|
@ -20,7 +20,7 @@ jobs:
|
|||
git config --unset "http.${GITHUB_SERVER_URL}/.extraHeader"
|
||||
git lfs install --local
|
||||
git lfs pull
|
||||
- name: Build
|
||||
- name: Build and test
|
||||
run: nix build -L .#li7y .#li7y-oci
|
||||
- name: Push OCI image
|
||||
if: github.ref == 'refs/heads/master'
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,5 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/target
|
||||
/.tarpaulin_target
|
||||
result*
|
||||
.pre-commit-config.yaml
|
||||
|
|
16
.tarpaulin.toml
Normal file
16
.tarpaulin.toml
Normal 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"
|
47
flake.nix
47
flake.nix
|
@ -74,12 +74,54 @@
|
|||
};
|
||||
};
|
||||
|
||||
packages = rec {
|
||||
li7y = naersk'.buildPackage {
|
||||
packages =
|
||||
let
|
||||
# naersk does not easily allow overrideAttrs
|
||||
commonNaerskConfigurarion = {
|
||||
src = self;
|
||||
|
||||
checkInputs = with pkgs; [
|
||||
postgresql
|
||||
postgresqlTestHook
|
||||
];
|
||||
|
||||
doCheck = true;
|
||||
|
||||
# tests need to be able to create and drop databases
|
||||
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";
|
||||
|
@ -99,6 +141,7 @@
|
|||
rustPackageDev
|
||||
] ++ (with pkgs; [
|
||||
cargo-deny
|
||||
cargo-tarpaulin
|
||||
cargo-watch
|
||||
clippy
|
||||
graphviz
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,3 +16,64 @@ impl ItemRepository {
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,3 +59,104 @@ impl ItemRepository {
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use sqlx::query;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{ItemRepository, ItemName, ItemPreview};
|
||||
use super::{ItemName, ItemPreview, ItemRepository};
|
||||
use crate::database::item_states::ItemState;
|
||||
|
||||
pub struct ItemDetails {
|
||||
|
|
|
@ -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;
|
||||
|
|
37
src/test_utils.rs
Normal file
37
src/test_utils.rs
Normal 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())
|
||||
}
|
Loading…
Reference in a new issue