Lines
76.53 %
Functions
15.17 %
Branches
100 %
pub mod user {
use crate::error::ServerError;
use crate::user::User;
use finance::tag::Tag;
use sqlx::types::Uuid;
impl User {
pub async fn create_tag(
&self,
name: String,
value: String,
description: Option<String>,
) -> Result<Uuid, ServerError> {
let mut conn = self.get_connection().await.map_err(|err| {
log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
ServerError::DB(err)
})?;
let id = Uuid::new_v4();
Tag {
id,
tag_name: name,
tag_value: value,
description,
}
.commit(&mut *conn)
.await
.map_err(|err| {
ServerError::Finance(err)
Ok(id)
pub async fn list_tags(&self) -> Result<Vec<Tag>, ServerError> {
let mut conn = self.get_connection().await?;
let tags = sqlx::query_file_as!(Tag, "sql/select/tags/all.sql")
.fetch_all(&mut *conn)
ServerError::DB(crate::db::DBError::Sqlx(err))
Ok(tags)
pub async fn get_tag(&self, id: Uuid) -> Result<Tag, ServerError> {
let tag = sqlx::query_file_as!(Tag, "sql/select/tags/by_id.sql", &id)
.fetch_one(&mut *conn)
Ok(tag)
pub async fn update_tag(
id: Uuid,
) -> Result<(), ServerError> {
sqlx::query_file!(
"sql/update/tags/update.sql",
&id,
&name,
&value,
description.as_deref()
)
.execute(&mut *conn)
Ok(())
pub async fn get_transaction_tags(&self, tx_id: Uuid) -> Result<Vec<Tag>, ServerError> {
let tags = sqlx::query_file_as!(Tag, "sql/select/tags/by_transaction.sql", &tx_id)
pub async fn create_transaction_tag(
tx_id: Uuid,
let tag_id = Uuid::new_v4();
let tag = Tag {
id: tag_id,
};
tag.commit(&mut *conn).await.map_err(|err| {
"sql/insert/transaction_tags/transaction_tag.sql",
&tx_id,
&tag_id
Ok(tag_id)
pub async fn delete_tag(&self, id: Uuid) -> Result<(), ServerError> {
sqlx::query_file!("sql/delete/tags/by_id.sql", &id)
pub async fn list_transaction_tag_names(&self) -> Result<Vec<String>, ServerError> {
let records = sqlx::query_file!("sql/select/tags/transaction/names.sql")
Ok(records.into_iter().map(|r| r.tag_name).collect())
pub async fn list_transaction_tag_values(
tag_name: &str,
) -> Result<Vec<String>, ServerError> {
let records =
sqlx::query_file!("sql/select/tags/transaction/values_by_name.sql", tag_name)
Ok(records.into_iter().map(|r| r.tag_value).collect())
pub async fn create_split_tag(
split_id: Uuid,
sqlx::query_file!("sql/insert/split_tags/split_tag.sql", &split_id, &tag_id)
pub async fn list_split_tag_names(&self) -> Result<Vec<String>, ServerError> {
let records = sqlx::query_file!("sql/select/tags/split/names.sql")
pub async fn list_split_tag_values(
let records = sqlx::query_file!("sql/select/tags/split/values_by_name.sql", tag_name)
#[cfg(test)]
mod tag_tests {
use super::*;
use crate::db::DB_POOL;
#[cfg(feature = "testlog")]
use env_logger;
use finance::{split::Split, transaction::Transaction};
use log;
use sqlx::PgPool;
use sqlx::types::chrono;
use supp_macro::local_db_sqlx_test;
use tokio::sync::OnceCell;
/// Context for keeping environment intact
static CONTEXT: OnceCell<()> = OnceCell::const_new();
static USER: OnceCell<User> = OnceCell::const_new();
async fn setup() {
CONTEXT
.get_or_init(|| async {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
})
.await;
USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
#[local_db_sqlx_test]
async fn test_tag_creation(pool: PgPool) -> Result<(), anyhow::Error> {
let user = USER.get().unwrap();
user.commit()
.expect("Failed to commit user to database");
let id = user
.create_tag("testtag".to_string(), "testval".to_string(), None)
.await?;
let mut conn = user.get_connection().await?;
let res = sqlx::query_file!("testdata/query_tag_by_id.sql", &id)
assert_eq!(res.tag_name, "testtag".to_string());
assert_eq!(res.tag_value, "testval".to_string());
assert_eq!(res.description, None);
async fn test_tag_creation_with_description(pool: PgPool) -> Result<(), anyhow::Error> {
.create_tag(
"categorytag".to_string(),
"category1".to_string(),
Some("Test description".to_string()),
let tag = user.get_tag(id).await?;
assert_eq!(tag.tag_name, "categorytag");
assert_eq!(tag.tag_value, "category1");
assert_eq!(tag.description, Some("Test description".to_string()));
async fn test_list_tags(pool: PgPool) -> Result<(), anyhow::Error> {
let id1 = user
.create_tag("tag1".to_string(), "value1".to_string(), None)
let id2 = user
"tag2".to_string(),
"value2".to_string(),
Some("desc".to_string()),
let tags = user.list_tags().await?;
assert!(tags.len() >= 2);
assert!(tags.iter().any(|t| t.id == id1));
assert!(tags.iter().any(|t| t.id == id2));
let tag1 = tags.iter().find(|t| t.id == id1).unwrap();
assert_eq!(tag1.tag_name, "tag1");
assert_eq!(tag1.tag_value, "value1");
assert_eq!(tag1.description, None);
let tag2 = tags.iter().find(|t| t.id == id2).unwrap();
assert_eq!(tag2.tag_name, "tag2");
assert_eq!(tag2.tag_value, "value2");
assert_eq!(tag2.description, Some("desc".to_string()));
async fn test_get_tag(pool: PgPool) -> Result<(), anyhow::Error> {
"gettag".to_string(),
"getvalue".to_string(),
Some("Get description".to_string()),
assert_eq!(tag.id, id);
assert_eq!(tag.tag_name, "gettag");
assert_eq!(tag.tag_value, "getvalue");
assert_eq!(tag.description, Some("Get description".to_string()));
async fn test_get_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
let nonexistent_id = Uuid::new_v4();
let result = user.get_tag(nonexistent_id).await;
assert!(result.is_err());
async fn test_update_tag(pool: PgPool) -> Result<(), anyhow::Error> {
.create_tag("oldname".to_string(), "oldvalue".to_string(), None)
user.update_tag(
"newname".to_string(),
"newvalue".to_string(),
Some("Updated description".to_string()),
assert_eq!(tag.tag_name, "newname");
assert_eq!(tag.tag_value, "newvalue");
assert_eq!(tag.description, Some("Updated description".to_string()));
async fn test_update_tag_remove_description(pool: PgPool) {
"tagname".to_string(),
"tagvalue".to_string(),
Some("Initial description".to_string()),
user.update_tag(id, "tagname".to_string(), "tagvalue".to_string(), None)
assert_eq!(tag.description, None);
async fn test_delete_tag(pool: PgPool) -> Result<(), anyhow::Error> {
.create_tag("deletetag".to_string(), "deletevalue".to_string(), None)
let tag = user.get_tag(id).await;
assert!(tag.is_ok());
user.delete_tag(id).await?;
let result = user.get_tag(id).await;
async fn test_delete_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
let result = user.delete_tag(nonexistent_id).await;
assert!(result.is_ok());
async fn test_list_tags_empty(pool: PgPool) -> Result<(), anyhow::Error> {
assert!(
tags.is_empty()
|| tags.iter().all(|t| t.tag_name == "name"
|| t.tag_name == "note"
|| t.tag_name == "symbol")
);
async fn test_list_transaction_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
let names = user.list_transaction_tag_names().await?;
assert!(names.is_empty());
async fn test_list_transaction_tag_names_with_data(
pool: PgPool,
) -> Result<(), anyhow::Error> {
let commodity_id = user
.create_commodity(100, "USD".to_string(), "US Dollar".to_string())
.await?
.id;
let acc1 = user.create_account("test_acc1", None).await?.id;
let acc2 = user.create_account("test_acc2", None).await?.id;
let tx = Transaction {
id: sqlx::types::Uuid::new_v4(),
post_date: chrono::Utc::now(),
enter_date: chrono::Utc::now(),
let mut ticket = tx.enter(&mut *conn).await?;
let split1 = Split {
account_id: acc1,
tx_id: tx.id,
value_num: 100,
value_denom: 1,
commodity_id,
reconcile_state: None,
reconcile_date: None,
lot_id: None,
let split2 = Split {
account_id: acc2,
value_num: -100,
ticket.add_splits(&[&split1, &split2]).await?;
ticket.commit().await?;
user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
user.create_transaction_tag(tx.id, "priority".to_string(), "high".to_string(), None)
assert_eq!(names.len(), 2);
assert!(names.contains(&"category".to_string()));
assert!(names.contains(&"priority".to_string()));
async fn test_list_transaction_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
let values = user.list_transaction_tag_values("category").await?;
assert!(values.is_empty());
async fn test_list_transaction_tag_values_with_data(
let tx1 = Transaction {
let tx2 = Transaction {
{
let mut ticket1 = tx1.enter(&mut *conn).await?;
let split1a = Split {
tx_id: tx1.id,
let split1b = Split {
ticket1.add_splits(&[&split1a, &split1b]).await?;
ticket1.commit().await?;
let mut ticket2 = tx2.enter(&mut *conn).await?;
let split2a = Split {
tx_id: tx2.id,
value_num: 200,
let split2b = Split {
value_num: -200,
ticket2.add_splits(&[&split2a, &split2b]).await?;
ticket2.commit().await?;
user.create_transaction_tag(tx1.id, "category".to_string(), "food".to_string(), None)
user.create_transaction_tag(
tx2.id,
"category".to_string(),
"transport".to_string(),
None,
user.create_transaction_tag(tx1.id, "priority".to_string(), "high".to_string(), None)
let category_values = user.list_transaction_tag_values("category").await?;
assert_eq!(category_values.len(), 2);
assert!(category_values.contains(&"food".to_string()));
assert!(category_values.contains(&"transport".to_string()));
let priority_values = user.list_transaction_tag_values("priority").await?;
assert_eq!(priority_values.len(), 1);
assert!(priority_values.contains(&"high".to_string()));
async fn test_list_transaction_tag_values_nonexistent_name(
let values = user.list_transaction_tag_values("nonexistent").await?;
async fn test_create_split_tag(pool: PgPool) -> Result<(), anyhow::Error> {
let tag_id = user
.create_split_tag(
split1.id,
"project".to_string(),
"nomisync".to_string(),
Some("Split tag for project tracking".to_string()),
let res = sqlx::query!(
"SELECT tag_id FROM split_tags WHERE split_id = $1",
&split1.id
assert_eq!(res.tag_id, tag_id);
let tag = user.get_tag(tag_id).await?;
assert_eq!(tag.tag_name, "project");
assert_eq!(tag.tag_value, "nomisync");
assert_eq!(
tag.description,
Some("Split tag for project tracking".to_string())
async fn test_list_split_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
let names = user.list_split_tag_names().await?;
async fn test_list_split_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
user.create_split_tag(
split2.id,
"department".to_string(),
"engineering".to_string(),
assert!(names.contains(&"project".to_string()));
assert!(names.contains(&"department".to_string()));
async fn test_list_split_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
let values = user.list_split_tag_values("project").await?;
async fn test_list_split_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
let split1_id = sqlx::types::Uuid::new_v4();
let split2_id = sqlx::types::Uuid::new_v4();
let split3_id = sqlx::types::Uuid::new_v4();
let split4_id = sqlx::types::Uuid::new_v4();
id: split1_id,
id: split2_id,
id: split3_id,
id: split4_id,
split1_id,
split3_id,
"website".to_string(),
split2_id,
let project_values = user.list_split_tag_values("project").await?;
assert_eq!(project_values.len(), 2);
assert!(project_values.contains(&"nomisync".to_string()));
assert!(project_values.contains(&"website".to_string()));
let department_values = user.list_split_tag_values("department").await?;
assert_eq!(department_values.len(), 1);
assert!(department_values.contains(&"engineering".to_string()));
async fn test_list_split_tag_values_nonexistent_name(
let values = user.list_split_tag_values("nonexistent").await?;