Add comprehensive TDD infrastructure with 45 tests

- 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
This commit is contained in:
2026-04-02 11:13:41 +02:00
parent 429d5d774f
commit e2123e4619
11 changed files with 1648 additions and 87 deletions

449
tests/repository_test.rs Normal file
View File

@@ -0,0 +1,449 @@
//! 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
}