added cloude generated tests

main
Mathias Rothenhaeusler 2026-05-14 18:31:13 +02:00
parent 0c10ac9748
commit 4706de1b42
25 changed files with 926 additions and 156 deletions

View File

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(cargo test *)"
]
}
}

50
CLAUDE.md 100644
View File

@ -0,0 +1,50 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
cargo build # debug build
cargo build --release # release build
cargo run -- <args> # run with args (see CLI section)
cargo check # fast type/borrow check without linking
cargo clippy # lint
```
No tests exist yet in this project.
## Configuration
The binary reads environment config from `~/.config/rcc/<env>` (dotenv format). Default env is `local`.
Required keys: `DB_USERNAME`, `DB_PASSWORD`
Optional keys: `DB_LOCATION` (default: localhost), `DB_PORT` (default: 3306), `DB` (default: efulfilment)
Pass `-e <env>` to select a config file (e.g., `-e stage` loads `~/.config/rcc/stage`).
## Architecture
Three-layer structure: **CLI → Service → Repository**, wired together in `container.rs`.
- `src/cli/command.rs` — Clap structs defining all subcommands (`Merch`, `Schema`, `Filter`, `Page`, `Job`, `Use`)
- `src/cli/controller.rs` — dispatches parsed args to the appropriate service
- `src/container.rs` — factory functions that construct `Repo → Service` pairs from a `Db` instance
- `src/database/db.rs``Db` wraps a `Arc<Mutex<Pool>>` (mysql); `Db::initialize(env)` loads the dotenv config and opens a connection pool
- `src/repository/` — one repo per domain entity; each holds a `Db` and executes raw MySQL queries
- `src/service/` — one service per domain; holds the corresponding repo, formats and prints results to stdout
- `src/entity/` — plain structs with `mysql::FromRow` derives representing DB rows
- `src/lib.rs` — exports the `TerminalSize` trait (used by services to adapt output width to the terminal)
## CLI Usage
```
rcc [-e <env>] <subcommand>
merch <search> # search merchants by name
schema <search> # list columns for a table
filter <id> [-a] [-c] [-l] # filter info; -a=all, -c=config, -l=log
page <id> # page/file info
job <id> [-l] # job info; -l=log
use <file_name> # find filters that use a given file/module
```

View File

@ -48,3 +48,93 @@ pub enum Commands {
file_name: String, file_name: String,
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
fn parse(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("parse failed")
}
#[test]
fn merch_command() {
let cli = parse(&["rcc", "merch", "acme"]);
assert!(matches!(cli.mode, Commands::Merch { search: Some(ref s) } if s == "acme"));
}
#[test]
fn schema_command() {
let cli = parse(&["rcc", "schema", "order_id"]);
assert!(matches!(cli.mode, Commands::Schema { search: Some(ref s) } if s == "order_id"));
}
#[test]
fn filter_command_defaults() {
let cli = parse(&["rcc", "filter", "42"]);
assert!(matches!(
cli.mode,
Commands::Filter { filter_id: 42, all: false, config: false, log: false }
));
}
#[test]
fn filter_command_all_flag() {
let cli = parse(&["rcc", "filter", "42", "--all"]);
assert!(matches!(cli.mode, Commands::Filter { all: true, .. }));
}
#[test]
fn filter_command_config_flag() {
let cli = parse(&["rcc", "filter", "42", "--config"]);
assert!(matches!(cli.mode, Commands::Filter { config: true, .. }));
}
#[test]
fn filter_command_log_flag() {
let cli = parse(&["rcc", "filter", "42", "--log"]);
assert!(matches!(cli.mode, Commands::Filter { log: true, .. }));
}
#[test]
fn page_command() {
let cli = parse(&["rcc", "page", "7"]);
assert!(matches!(cli.mode, Commands::Page { page_id: 7 }));
}
#[test]
fn job_command_default() {
let cli = parse(&["rcc", "job", "3"]);
assert!(matches!(cli.mode, Commands::Job { job_id: 3, log: false }));
}
#[test]
fn job_command_log_flag() {
let cli = parse(&["rcc", "job", "3", "--log"]);
assert!(matches!(cli.mode, Commands::Job { job_id: 3, log: true }));
}
#[test]
fn use_command() {
let cli = parse(&["rcc", "use", "my_module.php"]);
assert!(matches!(cli.mode, Commands::Use { ref file_name } if file_name == "my_module.php"));
}
#[test]
fn env_flag_is_captured() {
let cli = parse(&["rcc", "-e", "stage", "merch", "x"]);
assert_eq!(cli.env.as_deref(), Some("stage"));
}
#[test]
fn env_defaults_to_none_when_not_provided() {
let cli = parse(&["rcc", "merch", "x"]);
assert!(cli.env.is_none());
}
#[test]
fn unknown_subcommand_is_rejected() {
assert!(Cli::try_parse_from(["rcc", "bogus"]).is_err());
}
}

View File

@ -1,11 +1,6 @@
use crate::{ use std::io;
container,
database::db::Db, use crate::{container, database::db::Db};
service::{
file_service::FileService, filter_service::FilterService, job_service::JobService,
merchant_service::MerchantService, schema_service::SchemaService,
},
};
use super::command::{Cli, Commands}; use super::command::{Cli, Commands};
@ -18,14 +13,17 @@ enum CommandError {
} }
pub fn process_args(args: Cli, db: Db) -> Result<(), Box<dyn std::error::Error>> { pub fn process_args(args: Cli, db: Db) -> Result<(), Box<dyn std::error::Error>> {
let stdout = io::stdout();
let mut out = stdout.lock();
match args.mode { match args.mode {
Commands::Merch { search } => { Commands::Merch { search } => {
let mut merchant_service: MerchantService = container::get_merchant_service(db); let mut merchant_service = container::get_merchant_service(db);
merchant_service.get_merchant(&search.ok_or(CommandError::EmptySearch())?)?; merchant_service.get_merchant(&search.ok_or(CommandError::EmptySearch())?, &mut out)?;
} }
Commands::Schema { search } => { Commands::Schema { search } => {
let mut schema_service: SchemaService = container::get_schema_service(db); let mut schema_service = container::get_schema_service(db);
schema_service.get_columns(&search.ok_or(CommandError::EmptySearch())?)?; schema_service.get_columns(&search.ok_or(CommandError::EmptySearch())?, &mut out)?;
} }
Commands::Filter { Commands::Filter {
filter_id, filter_id,
@ -33,31 +31,31 @@ pub fn process_args(args: Cli, db: Db) -> Result<(), Box<dyn std::error::Error>>
all, all,
log, log,
} => { } => {
let mut filter_service: FilterService = container::get_filter_service(db); let mut filter_service = container::get_filter_service(db);
if config { if config {
filter_service.get_filter_configs(&filter_id)?; filter_service.get_filter_configs(&filter_id, &mut out)?;
} else if log { } else if log {
filter_service.get_filter_log(&filter_id, all)?; filter_service.get_filter_log(&filter_id, all, &mut out)?;
} else { } else {
filter_service.get_filter(&filter_id, all)?; filter_service.get_filter(&filter_id, all, &mut out)?;
} }
} }
Commands::Page { page_id } => { Commands::Page { page_id } => {
let mut file_service: FileService = container::get_page_service(db); let mut file_service = container::get_page_service(db);
file_service.get_page(&page_id)?; file_service.get_page(&page_id, &mut out)?;
} }
Commands::Job { job_id, log } => { Commands::Job { job_id, log } => {
let mut job_service: JobService = container::get_job_service(db); let mut job_service = container::get_job_service(db);
if log { if log {
job_service.get_job_log(&job_id)?; job_service.get_job_log(&job_id, &mut out)?;
} else { } else {
job_service.get_job_by_id(&job_id)?; job_service.get_job_by_id(&job_id, &mut out)?;
} }
} }
Commands::Use { file_name } => { Commands::Use { file_name } => {
let mut filter_service: FilterService = container::get_filter_service(db); let mut filter_service = container::get_filter_service(db);
filter_service.get_filter_uses(&file_name)?; filter_service.get_filter_uses(&file_name, &mut out)?;
}, }
} }
Ok(()) Ok(())

View File

@ -10,27 +10,27 @@ use crate::{
}, },
}; };
pub fn get_filter_service(pool: Db) -> FilterService { pub fn get_filter_service(pool: Db) -> FilterService<FilterRepo> {
let repo = FilterRepo::new(pool); let repo = FilterRepo::new(pool);
FilterService::new(repo) FilterService::new(repo)
} }
pub fn get_merchant_service(pool: Db) -> MerchantService { pub fn get_merchant_service(pool: Db) -> MerchantService<MerchantRepo> {
let repo = MerchantRepo::new(pool); let repo = MerchantRepo::new(pool);
MerchantService::new(repo) MerchantService::new(repo)
} }
pub fn get_schema_service(pool: Db) -> SchemaService { pub fn get_schema_service(pool: Db) -> SchemaService<SchemaRepo> {
let repo = SchemaRepo::new(pool); let repo = SchemaRepo::new(pool);
SchemaService::new(repo) SchemaService::new(repo)
} }
pub fn get_page_service(pool: Db) -> FileService { pub fn get_page_service(pool: Db) -> FileService<FileRepo> {
let repo = FileRepo::new(pool); let repo = FileRepo::new(pool);
FileService::new(repo) FileService::new(repo)
} }
pub(crate) fn get_job_service(pool: Db) -> JobService { pub fn get_job_service(pool: Db) -> JobService<JobRepo> {
let repo = JobRepo::new(pool); let repo = JobRepo::new(pool);
JobService::new(repo) JobService::new(repo)
} }

View File

@ -69,13 +69,26 @@ impl Db {
impl Drop for Db { impl Drop for Db {
fn drop(&mut self) { fn drop(&mut self) {
// The drop function is called when the Db instance goes out of scope
// This is where you can clean up resources, like returning connections to the pool
log::info!("Db instance is being dropped. Cleaning up resources."); log::info!("Db instance is being dropped. Cleaning up resources.");
// You might want to explicitly drop the Mutex guard to release the lock
// (This is not strictly necessary as the Mutex will be automatically dropped,
// but it can be useful for explicitness)
drop(self.pool.lock().unwrap()); drop(self.pool.lock().unwrap());
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initialize_fails_when_config_file_missing() {
let result = Db::initialize("__nonexistent_env__".to_string());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("__nonexistent_env__"), "error should mention the env name: {msg}");
}
#[test]
fn initialize_fails_for_empty_env_name() {
let result = Db::initialize(String::new());
assert!(result.is_err());
}
}

View File

@ -1,4 +1,4 @@
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page { pub struct Page {
pub m_id: usize, pub m_id: usize,
pub title: String, pub title: String,

View File

@ -1,4 +1,4 @@
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter { pub struct Filter {
pub filter_module_id: usize, pub filter_module_id: usize,
pub file_name: String, pub file_name: String,

View File

@ -1,5 +1,5 @@
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
#[derive(Debug, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct FilterConfig { pub struct FilterConfig {
pub attribute: String, pub attribute: String,
pub value1: String, pub value1: String,

View File

@ -1,6 +1,6 @@
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterLog { pub struct FilterLog {
pub run_ts: PrimitiveDateTime, pub run_ts: PrimitiveDateTime,
pub error_code: String, pub error_code: String,

View File

@ -1,4 +1,4 @@
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterUses { pub struct FilterUses {
pub filter_module_id: usize, pub filter_module_id: usize,
pub description: String, pub description: String,

View File

@ -1,12 +1,12 @@
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Job { pub struct Job {
pub file_name: String, pub file_name: String,
pub description: String, pub description: String,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct JobLog { pub struct JobLog {
pub content: String, pub content: String,
pub upd_ts: PrimitiveDateTime, pub upd_ts: PrimitiveDateTime,

View File

@ -1,5 +1,5 @@
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Merchant { pub struct Merchant {
pub m_id: usize, pub m_id: usize,
pub m_name: String, pub m_name: String,

View File

@ -1,5 +1,5 @@
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Schema { pub struct Schema {
pub table_name: String, pub table_name: String,
pub column_name: String, pub column_name: String,

View File

@ -6,3 +6,24 @@ pub trait TerminalSize {
terminal.cols.into() terminal.cols.into()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
struct Probe;
impl TerminalSize for Probe {}
#[test]
fn get_width_returns_positive_value() {
assert!(Probe.get_width() > 0);
}
#[test]
fn get_width_falls_back_to_150_when_no_tty() {
// In a non-TTY test environment termsize::get() returns None,
// so the fallback of 150 columns is used.
let w = Probe.get_width();
assert!(w >= 1);
}
}

View File

@ -2,11 +2,21 @@ use mysql::{params, prelude::Queryable};
use crate::{database::db::Db, entity::file::Page}; use crate::{database::db::Db, entity::file::Page};
pub trait FileRepoTrait {
fn find_page(&mut self, file_id: &usize) -> Result<Vec<Page>, mysql::Error>;
}
#[derive(Debug)] #[derive(Debug)]
pub struct FileRepo { pub struct FileRepo {
db_pool: Db, db_pool: Db,
} }
impl FileRepoTrait for FileRepo {
fn find_page(&mut self, file_id: &usize) -> Result<Vec<Page>, mysql::Error> {
self.find_page(file_id)
}
}
impl FileRepo { impl FileRepo {
pub fn new(db: Db) -> Self { pub fn new(db: Db) -> Self {
Self { db_pool: db } Self { db_pool: db }

View File

@ -9,11 +9,33 @@ use crate::{
}, },
}; };
pub trait FilterRepoTrait {
fn find_by_id(&mut self, filter_id: &usize) -> Result<Vec<Filter>, mysql::Error>;
fn find_filter_configs(&mut self, filter_id: &usize) -> Result<Vec<FilterConfig>, mysql::Error>;
fn find_filter_log(&mut self, filter_id: &usize) -> Result<Vec<FilterLog>, mysql::Error>;
fn find_filter_uses(&mut self, filter_file: &str) -> Result<Vec<FilterUses>, mysql::Error>;
}
#[derive(Debug)] #[derive(Debug)]
pub struct FilterRepo { pub struct FilterRepo {
db: Db, db: Db,
} }
impl FilterRepoTrait for FilterRepo {
fn find_by_id(&mut self, filter_id: &usize) -> Result<Vec<Filter>, mysql::Error> {
self.find_by_id(filter_id)
}
fn find_filter_configs(&mut self, filter_id: &usize) -> Result<Vec<FilterConfig>, mysql::Error> {
self.find_filter_configs(filter_id)
}
fn find_filter_log(&mut self, filter_id: &usize) -> Result<Vec<FilterLog>, mysql::Error> {
self.find_filter_log(filter_id)
}
fn find_filter_uses(&mut self, filter_file: &str) -> Result<Vec<FilterUses>, mysql::Error> {
self.find_filter_uses(filter_file)
}
}
impl FilterRepo { impl FilterRepo {
pub fn new(db: Db) -> Self { pub fn new(db: Db) -> Self {
Self { db } Self { db }

View File

@ -5,11 +5,25 @@ use crate::{
entity::job::{Job, JobLog}, entity::job::{Job, JobLog},
}; };
pub trait JobRepoTrait {
fn find_by_id(&mut self, job_id: &usize) -> Result<Vec<Job>, mysql::Error>;
fn find_log(&mut self, job_id: &usize) -> Result<Vec<JobLog>, mysql::Error>;
}
#[derive(Debug)] #[derive(Debug)]
pub struct JobRepo { pub struct JobRepo {
db: Db, db: Db,
} }
impl JobRepoTrait for JobRepo {
fn find_by_id(&mut self, job_id: &usize) -> Result<Vec<Job>, mysql::Error> {
self.find_by_id(job_id)
}
fn find_log(&mut self, job_id: &usize) -> Result<Vec<JobLog>, mysql::Error> {
self.find_log(job_id)
}
}
impl JobRepo { impl JobRepo {
pub fn new(db: Db) -> Self { pub fn new(db: Db) -> Self {
Self { db } Self { db }

View File

@ -2,10 +2,20 @@ use mysql::{params, prelude::Queryable};
use crate::{database::db::Db, entity::merchant::Merchant}; use crate::{database::db::Db, entity::merchant::Merchant};
pub trait MerchantRepoTrait {
fn find_by_name_or_id(&mut self, search: &str) -> Result<Vec<Merchant>, mysql::Error>;
}
pub struct MerchantRepo { pub struct MerchantRepo {
db_pool: Db, db_pool: Db,
} }
impl MerchantRepoTrait for MerchantRepo {
fn find_by_name_or_id(&mut self, search: &str) -> Result<Vec<Merchant>, mysql::Error> {
self.find_by_name_or_id(search)
}
}
impl MerchantRepo { impl MerchantRepo {
pub fn new(db_pool: Db) -> Self { pub fn new(db_pool: Db) -> Self {
Self { db_pool } Self { db_pool }

View File

@ -2,10 +2,20 @@ use mysql::{params, prelude::Queryable};
use crate::{database::db::Db, entity::schema::Schema}; use crate::{database::db::Db, entity::schema::Schema};
pub trait SchemaRepoTrait {
fn find_by_column(&mut self, column_name: &str) -> Result<Vec<Schema>, mysql::Error>;
}
pub struct SchemaRepo { pub struct SchemaRepo {
db: Db, db: Db,
} }
impl SchemaRepoTrait for SchemaRepo {
fn find_by_column(&mut self, column_name: &str) -> Result<Vec<Schema>, mysql::Error> {
self.find_by_column(column_name)
}
}
impl SchemaRepo { impl SchemaRepo {
pub fn new(db: Db) -> Self { pub fn new(db: Db) -> Self {
Self { db } Self { db }

View File

@ -1,32 +1,85 @@
use std::io::Write;
use rcc::TerminalSize; use rcc::TerminalSize;
use crate::{entity::file::Page, repository::file_repo::FileRepo}; use crate::{entity::file::Page, repository::file_repo::FileRepoTrait};
pub struct FileService { pub struct FileService<R> {
repo: FileRepo, repo: R,
} }
impl TerminalSize for FileService {} impl<R> TerminalSize for FileService<R> {}
impl FileService { impl<R: FileRepoTrait> FileService<R> {
pub fn new(repo: FileRepo) -> Self { pub fn new(repo: R) -> Self {
Self { repo } Self { repo }
} }
pub fn get_page(&mut self, page_id: &usize) -> Result<(), Box<dyn std::error::Error>> { pub fn get_page(&mut self, page_id: &usize, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let page = self.repo.find_page(page_id)?; let page = self.repo.find_page(page_id)?;
if page.is_empty() { if page.is_empty() {
println!("No page found."); writeln!(out, "No page found.")?;
} else { } else {
page.into_iter().for_each(|page: Page| { page.into_iter().for_each(|page: Page| {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width())).ok();
println!("M-ID: {}", page.m_id); writeln!(out, "M-ID: {}", page.m_id).ok();
println!("FileName: {}", page.file); writeln!(out, "FileName: {}", page.file).ok();
println!("Title: {}", page.title); writeln!(out, "Title: {}", page.title).ok();
}); });
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
} }
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{entity::file::Page, repository::file_repo::FileRepoTrait};
struct MockFileRepo {
pages: Vec<Page>,
}
impl FileRepoTrait for MockFileRepo {
fn find_page(&mut self, _: &usize) -> Result<Vec<Page>, mysql::Error> {
Ok(std::mem::take(&mut self.pages))
}
}
fn run(repo: MockFileRepo) -> String {
let mut svc = FileService::new(repo);
let mut out = Vec::new();
svc.get_page(&1, &mut out).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn not_found_message() {
let out = run(MockFileRepo { pages: vec![] });
assert_eq!(out, "No page found.\n");
}
#[test]
fn prints_page_fields() {
let out = run(MockFileRepo {
pages: vec![Page { m_id: 3, title: "Home".into(), file: "index.php".into() }],
});
assert!(out.contains("M-ID: 3"));
assert!(out.contains("FileName: index.php"));
assert!(out.contains("Title: Home"));
}
#[test]
fn multiple_pages_all_printed() {
let out = run(MockFileRepo {
pages: vec![
Page { m_id: 1, title: "A".into(), file: "a.php".into() },
Page { m_id: 2, title: "B".into(), file: "b.php".into() },
],
});
assert!(out.contains("a.php"));
assert!(out.contains("b.php"));
}
}

View File

@ -1,47 +1,43 @@
use std::io::Write;
use colored::Colorize; use colored::Colorize;
use rcc::TerminalSize; use rcc::TerminalSize;
use crate::{ use crate::{
entity::{filter::Filter, filter_config::FilterConfig, filter_log::FilterLog, filter_uses::FilterUses}, entity::{filter::Filter, filter_config::FilterConfig, filter_log::FilterLog, filter_uses::FilterUses},
repository::filter_repo::FilterRepo, repository::filter_repo::FilterRepoTrait,
}; };
pub struct FilterService { pub struct FilterService<R> {
repo: FilterRepo, repo: R,
} }
impl TerminalSize for FilterService {} impl<R> TerminalSize for FilterService<R> {}
impl FilterService { impl<R: FilterRepoTrait> FilterService<R> {
pub fn new(repo: FilterRepo) -> Self { pub fn new(repo: R) -> Self {
Self { repo } Self { repo }
} }
pub fn get_filter_uses(&mut self, file_name: &str) -> Result<(), Box<dyn std::error::Error>> { pub fn get_filter_uses(&mut self, file_name: &str, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let uses = self.repo.find_filter_uses(file_name)?; let uses = self.repo.find_filter_uses(file_name)?;
if uses.is_empty() { if uses.is_empty() {
println!("No use for file/module found!"); writeln!(out, "No use for file/module found!")?;
} else { } else {
uses.into_iter().for_each(|filter_use: FilterUses| { uses.into_iter().for_each(|filter_use: FilterUses| {
let is_active = if filter_use.active == 1 { let is_active = if filter_use.active == 1 { "true" } else { "false" };
"true".to_string() writeln!(out, "{}", "-".repeat(self.get_width())).ok();
} else { writeln!(out, "Module ID: {}", filter_use.filter_module_id).ok();
"false".to_string() writeln!(out, "ModuleNo: {}", filter_use.filter_module_no).ok();
}; writeln!(out, "FilterUser: {}", filter_use.filter_user).ok();
println!("{}", "-".repeat(self.get_width())); writeln!(out, "Description: {}", filter_use.description).ok();
writeln!(out, "EP_NO: {}", filter_use.ep_no).ok();
println!("Module ID: {}", filter_use.filter_module_id); writeln!(out, "FilterId: {}", filter_use.filter_id).ok();
println!("ModuleNo: {}", filter_use.filter_module_no); writeln!(out, "FilterNo: {}", filter_use.filter_no).ok();
println!("FilterUser: {}", filter_use.filter_user); writeln!(out, "Merchant: {}", filter_use.merchant).ok();
println!("Description: {}", filter_use.description); writeln!(out, "Active: {}", is_active).ok();
println!("EP_NO: {}", filter_use.ep_no);
println!("FilterId: {}", filter_use.filter_id);
println!("FilterNo: {}", filter_use.filter_no);
println!("Merchant: {}", filter_use.merchant);
println!("Active: {}", is_active);
}); });
} }
Ok(()) Ok(())
@ -51,25 +47,26 @@ impl FilterService {
&mut self, &mut self,
filter_id: &usize, filter_id: &usize,
all: bool, all: bool,
out: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let filters = self.repo.find_by_id(filter_id)?; let filters = self.repo.find_by_id(filter_id)?;
if filters.is_empty() { if filters.is_empty() {
println!("Filter not found!"); writeln!(out, "Filter not found!")?;
} else { } else {
filters.into_iter().for_each(|filter: Filter| { filters.into_iter().for_each(|filter: Filter| {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width())).ok();
println!("Module ID: {}", filter.filter_module_id); writeln!(out, "Module ID: {}", filter.filter_module_id).ok();
println!("FileName: {}", filter.file_name); writeln!(out, "FileName: {}", filter.file_name).ok();
println!("Description: {}", filter.description); writeln!(out, "Description: {}", filter.description).ok();
println!("ModuleNo: {}", filter.filter_module_no); writeln!(out, "ModuleNo: {}", filter.filter_module_no).ok();
println!("FilterUser: {}", filter.filter_user); writeln!(out, "FilterUser: {}", filter.filter_user).ok();
}); });
if all { if all {
self.get_filter_configs(filter_id)? self.get_filter_configs(filter_id, out)?;
} else { } else {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
} }
}; };
@ -79,26 +76,28 @@ impl FilterService {
pub fn get_filter_configs( pub fn get_filter_configs(
&mut self, &mut self,
filter_id: &usize, filter_id: &usize,
out: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let filter_configs = self.repo.find_filter_configs(filter_id)?; let filter_configs = self.repo.find_filter_configs(filter_id)?;
if filter_configs.is_empty() { if filter_configs.is_empty() {
println!("No filter configs found!"); writeln!(out, "No filter configs found!")?;
} else { } else {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
filter_configs filter_configs
.into_iter() .into_iter()
.for_each(|filter_config: FilterConfig| { .for_each(|filter_config: FilterConfig| {
println!( writeln!(
out,
"{0: <25} | {1: <20} | {2: <20} | {3: <10} | {4: <10}", "{0: <25} | {1: <20} | {2: <20} | {3: <10} | {4: <10}",
filter_config.attribute, filter_config.attribute,
filter_config.value1, filter_config.value1,
filter_config.value2.unwrap_or("n/a".to_string()), filter_config.value2.unwrap_or("n/a".to_string()),
filter_config.upd_ts, filter_config.upd_ts,
filter_config.name filter_config.name
); ).ok();
}); });
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
}; };
Ok(()) Ok(())
@ -108,45 +107,329 @@ impl FilterService {
&mut self, &mut self,
filter_id: &usize, filter_id: &usize,
all: bool, all: bool,
out: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let filter_log = self.repo.find_filter_log(filter_id)?; let filter_log = self.repo.find_filter_log(filter_id)?;
if filter_log.is_empty() { if filter_log.is_empty() {
println!("No filter log found!"); writeln!(out, "No filter log found!")?;
} else { } else {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
filter_log.into_iter().for_each(|filter_log: FilterLog| { filter_log.into_iter().for_each(|filter_log: FilterLog| {
println!( writeln!(out, "TS: {} Error Code: {}", filter_log.run_ts, filter_log.error_code).ok();
"TS: {} Error Code: {}",
filter_log.run_ts, filter_log.error_code
);
match filter_log.error_code.as_str() { match filter_log.error_code.as_str() {
"WARNING" => { "WARNING" => {
println!("{}", filter_log.error_msg.yellow()); writeln!(out, "{}", filter_log.error_msg.yellow()).ok();
if all { if all {
println!("{}", filter_log.mysql_error.yellow()); writeln!(out, "{}", filter_log.mysql_error.yellow()).ok();
} }
} }
"ERROR" => { "ERROR" => {
println!("{}", filter_log.error_msg.red()); writeln!(out, "{}", filter_log.error_msg.red()).ok();
if all { if all {
println!("{}", filter_log.mysql_error.red()); writeln!(out, "{}", filter_log.mysql_error.red()).ok();
} }
} }
"DEBUG" => { "DEBUG" => {
println!("{}", filter_log.error_msg.green()); writeln!(out, "{}", filter_log.error_msg.green()).ok();
if all { if all {
println!("{}", filter_log.error_msg.green()) writeln!(out, "{}", filter_log.error_msg.green()).ok();
} }
} }
_ => { _ => {
println!("{}", filter_log.error_msg); writeln!(out, "{}", filter_log.error_msg).ok();
} }
} }
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
println!("{}", "-".repeat(self.get_width()));
}) })
} }
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
entity::{
filter::Filter, filter_config::FilterConfig, filter_log::FilterLog,
filter_uses::FilterUses,
},
repository::filter_repo::FilterRepoTrait,
};
use time::{Date, Month, PrimitiveDateTime, Time};
fn test_dt() -> PrimitiveDateTime {
PrimitiveDateTime::new(
Date::from_calendar_date(2024, Month::January, 1).unwrap(),
Time::MIDNIGHT,
)
}
struct MockFilterRepo {
filters: Vec<Filter>,
configs: Vec<FilterConfig>,
logs: Vec<FilterLog>,
uses: Vec<FilterUses>,
}
impl MockFilterRepo {
fn empty() -> Self {
Self { filters: vec![], configs: vec![], logs: vec![], uses: vec![] }
}
}
impl FilterRepoTrait for MockFilterRepo {
fn find_by_id(&mut self, _: &usize) -> Result<Vec<Filter>, mysql::Error> {
Ok(std::mem::take(&mut self.filters))
}
fn find_filter_configs(&mut self, _: &usize) -> Result<Vec<FilterConfig>, mysql::Error> {
Ok(std::mem::take(&mut self.configs))
}
fn find_filter_log(&mut self, _: &usize) -> Result<Vec<FilterLog>, mysql::Error> {
Ok(std::mem::take(&mut self.logs))
}
fn find_filter_uses(&mut self, _: &str) -> Result<Vec<FilterUses>, mysql::Error> {
Ok(std::mem::take(&mut self.uses))
}
}
fn capture(f: impl FnOnce(&mut FilterService<MockFilterRepo>, &mut Vec<u8>)) -> String {
colored::control::set_override(false);
let repo = MockFilterRepo::empty();
let mut svc = FilterService::new(repo);
let mut out = Vec::new();
f(&mut svc, &mut out);
String::from_utf8(out).unwrap()
}
fn capture_with(
repo: MockFilterRepo,
f: impl FnOnce(&mut FilterService<MockFilterRepo>, &mut Vec<u8>),
) -> String {
colored::control::set_override(false);
let mut svc = FilterService::new(repo);
let mut out = Vec::new();
f(&mut svc, &mut out);
String::from_utf8(out).unwrap()
}
// --- get_filter ---
#[test]
fn get_filter_not_found() {
let out = capture(|svc, out| { svc.get_filter(&1, false, out).unwrap(); });
assert_eq!(out, "Filter not found!\n");
}
#[test]
fn get_filter_prints_fields() {
let repo = MockFilterRepo {
filters: vec![Filter {
filter_module_id: 42,
file_name: "foo.php".into(),
description: "My filter".into(),
filter_module_no: "FM-7".into(),
filter_user: 99,
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter(&1, false, out).unwrap(); });
assert!(out.contains("Module ID: 42"));
assert!(out.contains("FileName: foo.php"));
assert!(out.contains("Description: My filter"));
assert!(out.contains("ModuleNo: FM-7"));
assert!(out.contains("FilterUser: 99"));
}
#[test]
fn get_filter_all_calls_configs() {
let repo = MockFilterRepo {
filters: vec![Filter {
filter_module_id: 1,
file_name: "f.php".into(),
description: "d".into(),
filter_module_no: "FM-1".into(),
filter_user: 1,
}],
configs: vec![FilterConfig {
attribute: "attr1".into(),
value1: "val1".into(),
value2: None,
name: "user1".into(),
upd_ts: test_dt(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter(&1, true, out).unwrap(); });
assert!(out.contains("attr1"));
assert!(out.contains("val1"));
}
// --- get_filter_configs ---
#[test]
fn get_filter_configs_empty() {
let out = capture(|svc, out| { svc.get_filter_configs(&1, out).unwrap(); });
assert_eq!(out, "No filter configs found!\n");
}
#[test]
fn get_filter_configs_prints_fields() {
let repo = MockFilterRepo {
configs: vec![FilterConfig {
attribute: "colour".into(),
value1: "blue".into(),
value2: Some("dark".into()),
name: "admin".into(),
upd_ts: test_dt(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_configs(&1, out).unwrap(); });
assert!(out.contains("colour"));
assert!(out.contains("blue"));
assert!(out.contains("dark"));
assert!(out.contains("admin"));
}
#[test]
fn get_filter_configs_none_value2_shows_na() {
let repo = MockFilterRepo {
configs: vec![FilterConfig {
attribute: "x".into(),
value1: "y".into(),
value2: None,
name: "n".into(),
upd_ts: test_dt(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_configs(&1, out).unwrap(); });
assert!(out.contains("n/a"));
}
// --- get_filter_log ---
#[test]
fn get_filter_log_empty() {
let out = capture(|svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
assert_eq!(out, "No filter log found!\n");
}
#[test]
fn get_filter_log_shows_error_code_and_msg() {
let repo = MockFilterRepo {
logs: vec![FilterLog {
run_ts: test_dt(),
error_code: "ERROR".into(),
error_msg: "something broke".into(),
mysql_error: "sql err".into(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
assert!(out.contains("Error Code: ERROR"));
assert!(out.contains("something broke"));
assert!(!out.contains("sql err"));
}
#[test]
fn get_filter_log_all_shows_mysql_error() {
let repo = MockFilterRepo {
logs: vec![FilterLog {
run_ts: test_dt(),
error_code: "WARNING".into(),
error_msg: "warn msg".into(),
mysql_error: "mysql detail".into(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, true, out).unwrap(); });
assert!(out.contains("warn msg"));
assert!(out.contains("mysql detail"));
}
#[test]
fn get_filter_log_debug_code() {
let repo = MockFilterRepo {
logs: vec![FilterLog {
run_ts: test_dt(),
error_code: "DEBUG".into(),
error_msg: "debug info".into(),
mysql_error: "".into(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
assert!(out.contains("debug info"));
}
#[test]
fn get_filter_log_unknown_code_printed_plain() {
let repo = MockFilterRepo {
logs: vec![FilterLog {
run_ts: test_dt(),
error_code: "INFO".into(),
error_msg: "plain message".into(),
mysql_error: "".into(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
assert!(out.contains("plain message"));
}
// --- get_filter_uses ---
#[test]
fn get_filter_uses_empty() {
let out = capture(|svc, out| { svc.get_filter_uses("foo.php", out).unwrap(); });
assert_eq!(out, "No use for file/module found!\n");
}
#[test]
fn get_filter_uses_prints_fields() {
let repo = MockFilterRepo {
uses: vec![FilterUses {
filter_module_id: 7,
description: "desc".into(),
filter_module_no: "FM-2".into(),
filter_user: 3,
filter_id: 10,
active: 1,
merchant: "Shop A".into(),
ep_id: 5,
ep_no: "EP-9".into(),
filter_no: "F-11".into(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_uses("foo.php", out).unwrap(); });
assert!(out.contains("Module ID: 7"));
assert!(out.contains("Merchant: Shop A"));
assert!(out.contains("Active: true"));
assert!(out.contains("EP_NO: EP-9"));
}
#[test]
fn get_filter_uses_inactive_shows_false() {
let repo = MockFilterRepo {
uses: vec![FilterUses {
filter_module_id: 1,
description: "d".into(),
filter_module_no: "FM".into(),
filter_user: 1,
filter_id: 1,
active: 0,
merchant: "M".into(),
ep_id: 1,
ep_no: "E".into(),
filter_no: "F".into(),
}],
..MockFilterRepo::empty()
};
let out = capture_with(repo, |svc, out| { svc.get_filter_uses("x", out).unwrap(); });
assert!(out.contains("Active: false"));
}
}

View File

@ -1,52 +1,135 @@
use std::io::Write;
use rcc::TerminalSize; use rcc::TerminalSize;
use crate::{ use crate::{
entity::job::{Job, JobLog}, entity::job::{Job, JobLog},
repository::job_repo::JobRepo, repository::job_repo::JobRepoTrait,
}; };
pub struct JobService { pub struct JobService<R> {
pub repo: JobRepo, pub repo: R,
} }
impl TerminalSize for JobService {} impl<R> TerminalSize for JobService<R> {}
impl JobService { impl<R: JobRepoTrait> JobService<R> {
pub fn new(repo: JobRepo) -> Self { pub fn new(repo: R) -> Self {
Self { repo } Self { repo }
} }
pub fn get_job_by_id(&mut self, job_id: &usize) -> Result<(), Box<dyn std::error::Error>> { pub fn get_job_by_id(&mut self, job_id: &usize, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let result = self.repo.find_by_id(job_id)?; let result = self.repo.find_by_id(job_id)?;
if result.is_empty() { if result.is_empty() {
println!("Job not found!"); writeln!(out, "Job not found!")?;
} else { } else {
result.into_iter().for_each(|job: Job| { result.into_iter().for_each(|job: Job| {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width())).ok();
println!("FileName: {}", job.file_name); writeln!(out, "FileName: {}", job.file_name).ok();
println!("Description: {}", job.description); writeln!(out, "Description: {}", job.description).ok();
}); });
writeln!(out, "{}", "-".repeat(self.get_width()))?;
println!("{}", "-".repeat(self.get_width()));
} }
Ok(()) Ok(())
} }
pub fn get_job_log(&mut self, job_id: &usize) -> Result<(), Box<dyn std::error::Error>> { pub fn get_job_log(&mut self, job_id: &usize, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let result = self.repo.find_log(job_id)?; let result = self.repo.find_log(job_id)?;
if result.is_empty() { if result.is_empty() {
println!("Job log not found!"); writeln!(out, "Job log not found!")?;
} else { } else {
result.into_iter().for_each(|joblog: JobLog| { result.into_iter().for_each(|joblog: JobLog| {
println!("{0: <25} | {1: <20} ", joblog.upd_ts, joblog.content,); writeln!(out, "{0: <25} | {1: <20} ", joblog.upd_ts, joblog.content).ok();
}); });
writeln!(out, "{}", "-".repeat(self.get_width()))?;
println!("{}", "-".repeat(self.get_width()));
} }
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
entity::job::{Job, JobLog},
repository::job_repo::JobRepoTrait,
};
use time::{Date, Month, PrimitiveDateTime, Time};
fn test_dt() -> PrimitiveDateTime {
PrimitiveDateTime::new(
Date::from_calendar_date(2024, Month::June, 15).unwrap(),
Time::MIDNIGHT,
)
}
struct MockJobRepo {
jobs: Vec<Job>,
logs: Vec<JobLog>,
}
impl MockJobRepo {
fn empty() -> Self {
Self { jobs: vec![], logs: vec![] }
}
}
impl JobRepoTrait for MockJobRepo {
fn find_by_id(&mut self, _: &usize) -> Result<Vec<Job>, mysql::Error> {
Ok(std::mem::take(&mut self.jobs))
}
fn find_log(&mut self, _: &usize) -> Result<Vec<JobLog>, mysql::Error> {
Ok(std::mem::take(&mut self.logs))
}
}
fn run_job(repo: MockJobRepo) -> String {
let mut svc = JobService::new(repo);
let mut out = Vec::new();
svc.get_job_by_id(&1, &mut out).unwrap();
String::from_utf8(out).unwrap()
}
fn run_log(repo: MockJobRepo) -> String {
let mut svc = JobService::new(repo);
let mut out = Vec::new();
svc.get_job_log(&1, &mut out).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn job_not_found() {
let out = run_job(MockJobRepo::empty());
assert_eq!(out, "Job not found!\n");
}
#[test]
fn job_prints_fields() {
let out = run_job(MockJobRepo {
jobs: vec![Job { file_name: "import.php".into(), description: "Imports stuff".into() }],
..MockJobRepo::empty()
});
assert!(out.contains("FileName: import.php"));
assert!(out.contains("Description: Imports stuff"));
}
#[test]
fn job_log_not_found() {
let out = run_log(MockJobRepo::empty());
assert_eq!(out, "Job log not found!\n");
}
#[test]
fn job_log_prints_content_and_timestamp() {
let out = run_log(MockJobRepo {
logs: vec![JobLog { content: "log line".into(), upd_ts: test_dt() }],
..MockJobRepo::empty()
});
assert!(out.contains("log line"));
assert!(out.contains("2024-06-15"));
}
}

View File

@ -1,32 +1,80 @@
use std::io::Write;
use rcc::TerminalSize; use rcc::TerminalSize;
use crate::repository::merchant_repo::MerchantRepo; use crate::{entity::merchant::Merchant, repository::merchant_repo::MerchantRepoTrait};
pub struct MerchantService { pub struct MerchantService<R> {
repo: MerchantRepo, repo: R,
} }
impl TerminalSize for MerchantService {} impl<R> TerminalSize for MerchantService<R> {}
impl MerchantService { impl<R: MerchantRepoTrait> MerchantService<R> {
pub fn new(repo: MerchantRepo) -> Self { pub fn new(repo: R) -> Self {
Self { repo } Self { repo }
} }
pub fn get_merchant(&mut self, search: &str) -> Result<(), Box<dyn std::error::Error>> { pub fn get_merchant(&mut self, search: &str, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let merchants = self.repo.find_by_name_or_id(search)?; let merchants = self.repo.find_by_name_or_id(search)?;
if merchants.is_empty() { if merchants.is_empty() {
println!("Merchant not found!"); writeln!(out, "Merchant not found!")?;
} }
merchants.into_iter().for_each(|merchant| { merchants.into_iter().for_each(|merchant: Merchant| {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width())).ok();
println!("Merchant: {}", merchant.m_name); writeln!(out, "Merchant: {}", merchant.m_name).ok();
println!("M-ID: {}", merchant.m_id); writeln!(out, "M-ID: {}", merchant.m_id).ok();
}); });
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{entity::merchant::Merchant, repository::merchant_repo::MerchantRepoTrait};
struct MockMerchantRepo {
merchants: Vec<Merchant>,
}
impl MerchantRepoTrait for MockMerchantRepo {
fn find_by_name_or_id(&mut self, _: &str) -> Result<Vec<Merchant>, mysql::Error> {
Ok(std::mem::take(&mut self.merchants))
}
}
fn run(repo: MockMerchantRepo) -> String {
let mut svc = MerchantService::new(repo);
let mut out = Vec::new();
svc.get_merchant("any", &mut out).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn not_found_message() {
let out = run(MockMerchantRepo { merchants: vec![] });
assert!(out.contains("Merchant not found!"));
}
#[test]
fn prints_merchant_fields() {
let out = run(MockMerchantRepo {
merchants: vec![Merchant { m_id: 5, m_name: "Acme".into() }],
});
assert!(out.contains("Merchant: Acme"));
assert!(out.contains("M-ID: 5"));
}
#[test]
fn prints_separator_after_results() {
let out = run(MockMerchantRepo {
merchants: vec![Merchant { m_id: 1, m_name: "X".into() }],
});
assert!(out.contains('-'));
}
}

View File

@ -1,32 +1,90 @@
use std::io::Write;
use rcc::TerminalSize; use rcc::TerminalSize;
use crate::repository::schema_repo::SchemaRepo; use crate::{entity::schema::Schema, repository::schema_repo::SchemaRepoTrait};
pub struct SchemaService { pub struct SchemaService<R> {
repo: SchemaRepo, repo: R,
} }
impl TerminalSize for SchemaService {} impl<R> TerminalSize for SchemaService<R> {}
impl SchemaService { impl<R: SchemaRepoTrait> SchemaService<R> {
pub fn new(repo: SchemaRepo) -> Self { pub fn new(repo: R) -> Self {
Self { repo } Self { repo }
} }
pub fn get_columns(&mut self, column: &str) -> Result<(), Box<dyn std::error::Error>> {
pub fn get_columns(&mut self, column: &str, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let columns = self.repo.find_by_column(column)?; let columns = self.repo.find_by_column(column)?;
if columns.is_empty() { if columns.is_empty() {
println!("No column found!"); writeln!(out, "No column found!")?;
} else { } else {
columns.into_iter().for_each(|column| { columns.into_iter().for_each(|column: Schema| {
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width())).ok();
println!("Table name: {}", column.table_name); writeln!(out, "Table name: {}", column.table_name).ok();
println!("Column name: {}", column.column_name); writeln!(out, "Column name: {}", column.column_name).ok();
println!("Type: {}", column.column_type); writeln!(out, "Type: {}", column.column_type).ok();
}); });
println!("{}", "-".repeat(self.get_width())); writeln!(out, "{}", "-".repeat(self.get_width()))?;
} }
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{entity::schema::Schema, repository::schema_repo::SchemaRepoTrait};
struct MockSchemaRepo {
columns: Vec<Schema>,
}
impl SchemaRepoTrait for MockSchemaRepo {
fn find_by_column(&mut self, _: &str) -> Result<Vec<Schema>, mysql::Error> {
Ok(std::mem::take(&mut self.columns))
}
}
fn run(repo: MockSchemaRepo) -> String {
let mut svc = SchemaService::new(repo);
let mut out = Vec::new();
svc.get_columns("any", &mut out).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn not_found_message() {
let out = run(MockSchemaRepo { columns: vec![] });
assert_eq!(out, "No column found!\n");
}
#[test]
fn prints_schema_fields() {
let out = run(MockSchemaRepo {
columns: vec![Schema {
table_name: "orders".into(),
column_name: "status".into(),
column_type: "varchar(32)".into(),
}],
});
assert!(out.contains("Table name: orders"));
assert!(out.contains("Column name: status"));
assert!(out.contains("Type: varchar(32)"));
}
#[test]
fn multiple_columns_all_printed() {
let out = run(MockSchemaRepo {
columns: vec![
Schema { table_name: "t1".into(), column_name: "c1".into(), column_type: "int".into() },
Schema { table_name: "t2".into(), column_name: "c2".into(), column_type: "text".into() },
],
});
assert!(out.contains("t1"));
assert!(out.contains("t2"));
}
}