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:
80
tests/common/fixtures.rs
Normal file
80
tests/common/fixtures.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
//! Test fixtures for CSV parsing tests.
|
||||
//!
|
||||
//! AI AGENT NOTE: These fixtures provide sample data for testing CSV parsing
|
||||
//! and other components without requiring real files.
|
||||
|
||||
/// Header row for CSV files.
|
||||
pub const CSV_HEADER: &str = "Date\tBatch number\tAmount\tVolume\tPrice\tQuality\tQualityName\tCard number\tCard type\tCustomer number\tStation\tTerminal\tPump\tReceipt\tCard report group number\tControl number";
|
||||
|
||||
/// A valid CSV row with a known customer (fleet account).
|
||||
///
|
||||
/// AI AGENT NOTE: This represents a typical fleet customer transaction.
|
||||
/// - Customer number: "1861" (known customer)
|
||||
/// - Card number: Full card number (not anonymized)
|
||||
/// - Amount: Positive (should be imported)
|
||||
pub const CSV_ROW_KNOWN_CUSTOMER: &str = "2026-02-01 10:15:16\t409\t559.26\t35.85\t15.60\t1001\t95 Oktan\t7825017523017000642\t7825017523017000642\t1861\t97254\t1\t2\t000910\t1\t";
|
||||
|
||||
/// A valid CSV row with an anonymized card (retail customer).
|
||||
///
|
||||
/// AI AGENT NOTE: This represents a retail transaction.
|
||||
/// - Customer number: "" (empty - anonymized)
|
||||
/// - Card number: Contains asterisks (partially masked)
|
||||
/// - Amount: Positive (should be imported)
|
||||
pub const CSV_ROW_ANONYMIZED: &str = "2026-02-01 06:40:14\t409\t267.23\t17.13\t15.60\t1001\t95 Oktan\t554477******9952\t554477******9952\t\t97254\t1\t2\t000898\t4\t756969";
|
||||
|
||||
/// A CSV row with zero amount (should be filtered out).
|
||||
///
|
||||
/// AI AGENT NOTE: Zero amounts typically represent authorizations
|
||||
/// that were never completed.
|
||||
pub const CSV_ROW_ZERO_AMOUNT: &str = "2026-02-01 06:40:14\t409\t0.00\t0.00\t15.60\t1001\t95 Oktan\t554477******9952\t554477******9952\t\t97254\t1\t2\t000898\t4\t756969";
|
||||
|
||||
/// A CSV row with negative amount (should be filtered out).
|
||||
///
|
||||
/// AI AGENT NOTE: Negative amounts typically represent cancellations
|
||||
/// or refunds.
|
||||
pub const CSV_ROW_NEGATIVE_AMOUNT: &str = "2026-02-01 06:40:14\t409\t-50.00\t-3.00\t15.60\t1001\t95 Oktan\t7825017523017000642\t7825017523017000642\t1861\t97254\t1\t2\t000898\t1\t";
|
||||
|
||||
/// A CSV row with US date format (MM/DD/YYYY).
|
||||
///
|
||||
/// AI AGENT NOTE: Some source files may use US date format.
|
||||
pub const CSV_ROW_US_DATE: &str = "02/01/2026 10:15:16 AM\t409\t559.26\t35.85\t15.60\t1001\t95 Oktan\t7825017523017000642\t7825017523017000642\t1861\t97254\t1\t2\t000910\t1\t";
|
||||
|
||||
/// Creates a multi-row CSV string for testing.
|
||||
///
|
||||
/// AI AGENT NOTE: Combines header and multiple data rows for
|
||||
/// testing full CSV parsing.
|
||||
pub fn create_test_csv(rows: &[&str]) -> String {
|
||||
let mut csv = CSV_HEADER.to_string();
|
||||
csv.push('\n');
|
||||
for row in rows {
|
||||
csv.push_str(row);
|
||||
csv.push('\n');
|
||||
}
|
||||
csv
|
||||
}
|
||||
|
||||
/// Sample CSV with mixed transactions (known, anonymized, etc.).
|
||||
pub fn sample_csv_mixed() -> String {
|
||||
create_test_csv(&[
|
||||
CSV_ROW_ANONYMIZED,
|
||||
CSV_ROW_KNOWN_CUSTOMER,
|
||||
CSV_ROW_ZERO_AMOUNT,
|
||||
])
|
||||
}
|
||||
|
||||
/// Sample CSV with only known customer transactions.
|
||||
pub fn sample_csv_known_only() -> String {
|
||||
create_test_csv(&[
|
||||
CSV_ROW_KNOWN_CUSTOMER,
|
||||
"2026-02-01 10:32:18\t409\t508.40\t32.59\t15.60\t1001\t95 Oktan\t7825017523017000717\t7825017523017000717\t1861\t97254\t1\t2\t000912\t1\t",
|
||||
"2026-02-01 10:57:33\t409\t174.41\t11.18\t15.60\t1001\t95 Oktan\t7825017523017001053\t7825017523017001053\t1980\t97254\t1\t1\t000913\t1\t",
|
||||
])
|
||||
}
|
||||
|
||||
/// Sample CSV with Diesel transaction.
|
||||
pub fn sample_csv_diesel() -> String {
|
||||
create_test_csv(&[
|
||||
"2026-02-01 10:05:16\t409\t543.22\t31.40\t17.30\t4\tDiesel\t673706*********0155\t673706*********0155\t\t97254\t1\t2\t000909\t4\tD00824",
|
||||
"2026-02-01 11:10:21\t409\t612.25\t35.39\t17.30\t4\tDiesel\t7825017523017000873\t7825017523017000873\t1866\t97254\t1\t1\t000916\t1\t",
|
||||
])
|
||||
}
|
||||
10
tests/common/mod.rs
Normal file
10
tests/common/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Common test utilities.
|
||||
//!
|
||||
//! AI AGENT NOTE: This module provides shared test infrastructure
|
||||
//! including database helpers and sample data fixtures.
|
||||
|
||||
pub mod fixtures;
|
||||
pub mod test_db;
|
||||
|
||||
pub use fixtures::*;
|
||||
pub use test_db::*;
|
||||
122
tests/common/test_db.rs
Normal file
122
tests/common/test_db.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
//! Test database utilities.
|
||||
//!
|
||||
//! AI AGENT NOTE: These helpers manage the test database connection pool.
|
||||
//! Uses rusty_petroleum_test database for all tests.
|
||||
|
||||
use sqlx::mysql::{MySqlPool, MySqlPoolOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Creates a connection pool to the test database.
|
||||
///
|
||||
/// AI AGENT NOTE: Uses config.toml or config.test.toml for connection details.
|
||||
/// The test database should be separate from dev/prod to avoid data conflicts.
|
||||
pub async fn create_test_pool() -> MySqlPool {
|
||||
let config = crate::config::Config::load(crate::config::Env::Test)
|
||||
.expect("Failed to load test config");
|
||||
|
||||
MySqlPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.acquire_timeout(Duration::from_secs(10))
|
||||
.connect(&config.database.connection_url())
|
||||
.await
|
||||
.expect("Failed to connect to test database")
|
||||
}
|
||||
|
||||
/// Resets the test database by dropping and recreating all tables.
|
||||
///
|
||||
/// AI AGENT NOTE: This is used before running tests to ensure a clean state.
|
||||
/// It uses the `rusty_petroleum_test` database.
|
||||
pub async fn reset_test_database() -> anyhow::Result<()> {
|
||||
let config = crate::config::Config::load(crate::config::Env::Test)?;
|
||||
let database_url = config.database.connection_url();
|
||||
let base_url = database_url.trim_end_matches(config.env.database_name());
|
||||
|
||||
let setup_pool = MySqlPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(base_url)
|
||||
.await?;
|
||||
|
||||
// Drop database if exists
|
||||
sqlx::query(&format!("DROP DATABASE IF EXISTS {}", config.env.database_name()))
|
||||
.execute(&setup_pool)
|
||||
.await?;
|
||||
|
||||
// Create fresh database
|
||||
sqlx::query(&format!("CREATE DATABASE {}", config.env.database_name()))
|
||||
.execute(&setup_pool)
|
||||
.await?;
|
||||
|
||||
drop(setup_pool);
|
||||
|
||||
// Now create tables
|
||||
let pool = create_test_pool().await;
|
||||
|
||||
// Create customers table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE customers (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
card_report_group TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_customer_number (customer_number)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// Create cards table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE cards (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
card_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
customer_id INT UNSIGNED NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_card_number (card_number),
|
||||
INDEX idx_customer_id (customer_id),
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// Create transactions table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE transactions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
transaction_date DATETIME NOT NULL,
|
||||
batch_number VARCHAR(20) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
volume DECIMAL(10,3) NOT NULL,
|
||||
price DECIMAL(8,4) NOT NULL,
|
||||
quality_code INT NOT NULL,
|
||||
quality_name VARCHAR(50) NOT NULL,
|
||||
card_number VARCHAR(50) NOT NULL,
|
||||
station VARCHAR(20) NOT NULL,
|
||||
terminal VARCHAR(10) NOT NULL,
|
||||
pump VARCHAR(10) NOT NULL,
|
||||
receipt VARCHAR(20) NOT NULL,
|
||||
control_number VARCHAR(20),
|
||||
customer_id INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_transaction_date (transaction_date),
|
||||
INDEX idx_batch_number (batch_number),
|
||||
INDEX idx_customer_id (customer_id),
|
||||
INDEX idx_card_number (card_number),
|
||||
INDEX idx_station (station)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
drop(pool);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user