- Add lib crate exposing modules for integration testing - Add dev-dependencies: tokio-test 0.4, tempfile - Refactor parse_csv_fields() as pure function for unit testing - Add field validation (minimum 16 fields required) - Fix repository last_insert_id using SELECT LAST_INSERT_ID() - Add 10 lib tests for CSV parsing and date formatting - Add 10 config tests for environment configuration - Add 7 import tests for CSV file parsing - Add 6 models tests for database structs - Add 12 repository tests for CRUD operations
450 lines
13 KiB
Rust
450 lines
13 KiB
Rust
//! Tests for the repository module.
|
|
//!
|
|
//! AI AGENT NOTE: These tests verify database operations using the test database.
|
|
//! Each test uses a transaction that is rolled back after the test completes.
|
|
|
|
use sqlx::Row;
|
|
|
|
async fn create_test_pool() -> sqlx::MySqlPool {
|
|
invoice_generator::db::create_pool(&std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
|
let config = invoice_generator::config::Config::load(invoice_generator::config::Env::Test).unwrap();
|
|
config.database.connection_url()
|
|
})).await.unwrap()
|
|
}
|
|
|
|
// ===== Customer Tests =====
|
|
|
|
#[tokio::test]
|
|
async fn customer_insert_returns_id() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST001")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let id: u64 = row.get("id");
|
|
assert!(id > 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn customer_find_existing() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST002")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer = sqlx::query_as::<_, invoice_generator::db::models::Customer>(
|
|
"SELECT id, customer_number, card_report_group, created_at, updated_at
|
|
FROM customers WHERE customer_number = ?",
|
|
)
|
|
.bind("TEST002")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(customer.customer_number, "TEST002");
|
|
assert_eq!(customer.card_report_group, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn customer_find_nonexistent() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
let customer = sqlx::query_as::<_, invoice_generator::db::models::Customer>(
|
|
"SELECT id, customer_number, card_report_group, created_at, updated_at
|
|
FROM customers WHERE customer_number = ?",
|
|
)
|
|
.bind("NONEXISTENT")
|
|
.fetch_optional(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(customer.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn customer_multiple_cards() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST003")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
|
.bind("CARD001")
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
|
.bind("CARD002")
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let row = sqlx::query("SELECT COUNT(*) as count FROM cards WHERE customer_id = ?")
|
|
.bind(customer_id)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let count: i64 = row.get("count");
|
|
assert_eq!(count, 2);
|
|
}
|
|
|
|
// ===== Card Tests =====
|
|
|
|
#[tokio::test]
|
|
async fn card_insert_with_customer() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST004")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
|
.bind("TESTCARD001")
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let card_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let id: u64 = card_row.get("id");
|
|
assert!(id > 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn card_find_by_number() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST005")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
|
.bind("TESTCARD002")
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let card = sqlx::query_as::<_, invoice_generator::db::models::Card>(
|
|
"SELECT id, card_number, customer_id, created_at, updated_at
|
|
FROM cards WHERE card_number = ?",
|
|
)
|
|
.bind("TESTCARD002")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(card.card_number, "TESTCARD002");
|
|
}
|
|
|
|
// ===== Transaction Tests =====
|
|
|
|
#[tokio::test]
|
|
async fn transaction_insert_single() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST006")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.50, 10.5, 9.57, 1001, '95 Oktan', 'CARD123', 'S001', 'T1', 'P1', 'R001', ?)",
|
|
)
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let tx_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let id: u64 = tx_row.get("id");
|
|
assert!(id > 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn transaction_insert_anonymized() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.50, 10.5, 9.57, 1001, '95 Oktan', 'ANON******1234', 'S001', 'T1', 'P1', 'R002', NULL)",
|
|
)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let tx_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let id: u64 = tx_row.get("id");
|
|
assert!(id > 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn transaction_count() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST007")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
for i in 0..5 {
|
|
sqlx::query(&format!(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', {}, 10.0, 10.0, 1001, '95 Oktan', 'CARD{}', 'S001', 'T1', 'P1', 'R00{}', ?)",
|
|
100.0 + i as f64,
|
|
i,
|
|
i
|
|
))
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let row = sqlx::query("SELECT COUNT(*) as count FROM transactions WHERE customer_id = ?")
|
|
.bind(customer_id)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let count: i64 = row.get("count");
|
|
assert_eq!(count, 5);
|
|
}
|
|
|
|
// ===== Query Tests =====
|
|
|
|
#[tokio::test]
|
|
async fn query_transactions_by_customer() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST008")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
for i in 0..3 {
|
|
sqlx::query(&format!(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 {}:00:00', 'TEST', 100.0, 10.0, 10.0, 1001, '95 Oktan', 'CARD{}', 'S001', 'T1', 'P1', 'R00{}', ?)",
|
|
10 + i,
|
|
i,
|
|
i
|
|
))
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let transactions = sqlx::query_as::<_, invoice_generator::db::models::Transaction>(
|
|
"SELECT t.id, t.transaction_date, t.batch_number, t.amount, t.volume, t.price, t.quality_code, t.quality_name, t.card_number, t.station, t.terminal, t.pump, t.receipt, t.control_number, t.customer_id, t.created_at
|
|
FROM transactions t
|
|
WHERE t.customer_id = ?",
|
|
)
|
|
.bind(customer_id)
|
|
.fetch_all(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(transactions.len(), 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn query_excludes_anonymous_from_customer_invoice() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST009")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.0, 10.0, 10.0, 1001, '95 Oktan', 'KNOWNCARD', 'S001', 'T1', 'P1', 'R001', ?)",
|
|
)
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 11:00:00', 'TEST', 50.0, 5.0, 10.0, 1001, '95 Oktan', 'ANON******9999', 'S001', 'T1', 'P1', 'R002', NULL)",
|
|
)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let row = sqlx::query(
|
|
"SELECT COUNT(*) as count FROM transactions WHERE customer_id = ?",
|
|
)
|
|
.bind(customer_id)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let count: i64 = row.get("count");
|
|
assert_eq!(count, 1); // Only the known transaction
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sales_summary_by_product() {
|
|
let pool = create_test_pool().await;
|
|
let mut tx = pool.begin().await.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
|
)
|
|
.bind("TEST010")
|
|
.bind(1u8)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
let customer_id: u32 = customer_row.get("id");
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.0, 10.0, 10.0, 1001, '95 Oktan', 'CARD001', 'S001', 'T1', 'P1', 'R001', ?)",
|
|
)
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 11:00:00', 'TEST', 50.0, 5.0, 10.0, 4, 'Diesel', 'CARD001', 'S001', 'T1', 'P1', 'R002', ?)",
|
|
)
|
|
.bind(customer_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
let summaries = sqlx::query_as::<_, invoice_generator::db::repository::ProductSummary>(
|
|
"SELECT quality_name, COUNT(*) as tx_count, SUM(amount) as total_amount, SUM(volume) as total_volume
|
|
FROM transactions
|
|
GROUP BY quality_name",
|
|
)
|
|
.fetch_all(&mut *tx)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(summaries.len(), 2); // Two products: 95 Oktan and Diesel
|
|
}
|