refactor(tests): add CLI test macros and deduplicate helpers

- Add `define_cli_auth_test!` macro for authentication-required tests
- Add `define_cli_list_test!` macro for unauthenticated list tests
- Replace 18 auth tests and 5 list tests with macro invocations
- Add `create_entity_cli()` generic, convert per-entity helpers to
  thin wrappers
- Move `create_bag` and `create_gear` from brews_cli.rs to helpers.rs
This commit is contained in:
Jon Seager 2026-02-06 14:27:43 +00:00
parent dcdeaee7ca
commit 1257630019
No known key found for this signature in database
11 changed files with 250 additions and 463 deletions

View file

@ -1,4 +1,7 @@
use super::helpers::{create_token, run_brewlog}; use super::helpers::{create_token, run_brewlog};
use crate::test_macros::define_cli_auth_test;
define_cli_auth_test!(backup_requires_auth, &["backup"]);
#[test] #[test]
fn backup_produces_valid_json() { fn backup_produces_valid_json() {
@ -25,13 +28,3 @@ fn backup_produces_valid_json() {
assert!(data["cafes"].is_array()); assert!(data["cafes"].is_array());
assert!(data["timeline_events"].is_array()); assert!(data["timeline_events"].is_array());
} }
#[test]
fn backup_requires_auth() {
let output = run_brewlog(&["backup"], &[]);
assert!(
!output.status.success(),
"backup command should fail without auth"
);
}

View file

@ -1,17 +1,23 @@
use crate::helpers::{create_roast, create_roaster, create_token, run_brewlog, server_info}; use crate::helpers::{create_roast, create_roaster, create_token, run_brewlog};
use crate::test_macros::{define_cli_auth_test, define_cli_list_test};
use serde_json::Value; use serde_json::Value;
#[test] define_cli_auth_test!(
fn test_add_bag_requires_authentication() { test_add_bag_requires_authentication,
let _ = server_info(); &["bag", "add", "--roast-id", "123", "--amount", "250.0"]
);
let output = run_brewlog( define_cli_auth_test!(
&["bag", "add", "--roast-id", "123", "--amount", "250.0"], test_update_bag_requires_authentication,
&[], &["bag", "update", "--id", "123", "--remaining", "100.0"]
); );
define_cli_auth_test!(
assert!(!output.status.success(), "bag add without auth should fail"); test_delete_bag_requires_authentication,
} &["bag", "delete", "--id", "123"]
);
define_cli_list_test!(
test_list_bags_works_without_authentication,
&["bag", "list"]
);
#[test] #[test]
fn test_add_bag_with_authentication() { fn test_add_bag_with_authentication() {
@ -44,21 +50,6 @@ fn test_add_bag_with_authentication() {
assert!(bag["id"].is_i64()); assert!(bag["id"].is_i64());
} }
#[test]
fn test_update_bag_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&["bag", "update", "--id", "123", "--remaining", "100.0"],
&[],
);
assert!(
!output.status.success(),
"bag update without auth should fail"
);
}
#[test] #[test]
fn test_update_bag_with_authentication() { fn test_update_bag_with_authentication() {
let token = create_token("test-update-bag"); let token = create_token("test-update-bag");
@ -96,14 +87,6 @@ fn test_update_bag_with_authentication() {
assert_eq!(updated_bag["closed"].as_bool(), Some(true)); assert_eq!(updated_bag["closed"].as_bool(), Some(true));
} }
#[test]
fn test_list_bags_works_without_authentication() {
let _ = server_info();
// Listing without roast_id might return all or empty, but should succeed (200 OK)
let output = run_brewlog(&["bag", "list"], &[]);
assert!(output.status.success());
}
#[test] #[test]
fn test_list_bags_shows_added_bag() { fn test_list_bags_shows_added_bag() {
let token = create_token("test-list-bags"); let token = create_token("test-list-bags");
@ -216,13 +199,6 @@ fn test_list_bags_shows_open_and_closed_bags() {
); );
} }
#[test]
fn test_delete_bag_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["bag", "delete", "--id", "123"], &[]);
assert!(!output.status.success());
}
#[test] #[test]
fn test_delete_bag_with_authentication() { fn test_delete_bag_with_authentication() {
let token = create_token("test-delete-bag"); let token = create_token("test-delete-bag");

View file

@ -1,55 +1,6 @@
use crate::helpers::{create_roast, create_roaster, create_token, run_brewlog}; use crate::helpers::{
create_bag, create_gear, create_roast, create_roaster, create_token, run_brewlog,
fn create_bag(roast_id: &str, token: &str) -> String { };
let output = run_brewlog(
&["bag", "add", "--roast-id", roast_id, "--amount", "250"],
&[("BREWLOG_TOKEN", token)],
);
if !output.status.success() {
panic!(
"Failed to create bag: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let bag: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
bag["id"]
.as_i64()
.expect("bag id should be numeric")
.to_string()
}
fn create_gear(category: &str, make: &str, model: &str, token: &str) -> String {
let output = run_brewlog(
&[
"gear",
"add",
"--category",
category,
"--make",
make,
"--model",
model,
],
&[("BREWLOG_TOKEN", token)],
);
if !output.status.success() {
panic!(
"Failed to create gear: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let gear: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
gear["id"]
.as_i64()
.expect("gear id should be numeric")
.to_string()
}
#[test] #[test]
fn brew_add_creates_brew_via_api() { fn brew_add_creates_brew_via_api() {

View file

@ -1,11 +1,9 @@
use crate::helpers::{create_cafe, create_token, run_brewlog, server_info}; use crate::helpers::{create_cafe, create_token, run_brewlog};
use crate::test_macros::{define_cli_auth_test, define_cli_list_test};
use serde_json::Value; use serde_json::Value;
#[test] define_cli_auth_test!(
fn test_add_cafe_requires_authentication() { test_add_cafe_requires_authentication,
let _ = server_info();
let output = run_brewlog(
&[ &[
"cafe", "cafe",
"add", "add",
@ -18,16 +16,28 @@ fn test_add_cafe_requires_authentication() {
"--latitude", "--latitude",
"51.5074", "51.5074",
"--longitude", "--longitude",
"-0.1278", "-0.1278"
], ]
&[], );
); define_cli_auth_test!(
test_delete_cafe_requires_authentication,
assert!( &["cafe", "delete", "--id", "some-id"]
!output.status.success(), );
"cafe add without auth should fail" define_cli_auth_test!(
); test_update_cafe_requires_authentication,
} &[
"cafe",
"update",
"--id",
"some-id",
"--name",
"Updated Name"
]
);
define_cli_list_test!(
test_list_cafes_works_without_authentication,
&["cafe", "list"]
);
#[test] #[test]
fn test_add_cafe_with_authentication() { fn test_add_cafe_with_authentication() {
@ -67,23 +77,6 @@ fn test_add_cafe_with_authentication() {
assert!(cafe["id"].is_i64(), "Should have an ID"); assert!(cafe["id"].is_i64(), "Should have an ID");
} }
#[test]
fn test_list_cafes_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["cafe", "list"], &[]);
assert!(
output.status.success(),
"cafe list should work without auth"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let cafes: Value = serde_json::from_str(&stdout).expect("Should output valid JSON array");
assert!(cafes.is_array(), "Should return an array");
}
#[test] #[test]
fn test_list_cafes_shows_added_cafe() { fn test_list_cafes_shows_added_cafe() {
let token = create_token("test-list-cafes"); let token = create_token("test-list-cafes");
@ -112,37 +105,3 @@ fn test_list_cafes_shows_added_cafe() {
.any(|c| c["id"].as_i64().unwrap().to_string() == cafe_id); .any(|c| c["id"].as_i64().unwrap().to_string() == cafe_id);
assert!(found, "Should find the added cafe in the list"); assert!(found, "Should find the added cafe in the list");
} }
#[test]
fn test_delete_cafe_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["cafe", "delete", "--id", "some-id"], &[]);
assert!(
!output.status.success(),
"cafe delete without auth should fail"
);
}
#[test]
fn test_update_cafe_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&[
"cafe",
"update",
"--id",
"some-id",
"--name",
"Updated Name",
],
&[],
);
assert!(
!output.status.success(),
"cafe update without auth should fail"
);
}

View file

@ -1,11 +1,9 @@
use crate::helpers::{create_token, run_brewlog, server_info}; use crate::helpers::{create_token, run_brewlog};
use crate::test_macros::{define_cli_auth_test, define_cli_list_test};
use serde_json::Value; use serde_json::Value;
#[test] define_cli_auth_test!(
fn test_add_gear_requires_authentication() { test_add_gear_requires_authentication,
let _ = server_info();
let output = run_brewlog(
&[ &[
"gear", "gear",
"add", "add",
@ -14,16 +12,21 @@ fn test_add_gear_requires_authentication() {
"--make", "--make",
"Baratza", "Baratza",
"--model", "--model",
"Encore", "Encore"
], ]
&[], );
); define_cli_auth_test!(
test_update_gear_requires_authentication,
assert!( &["gear", "update", "--id", "123", "--make", "Updated"]
!output.status.success(), );
"gear add without auth should fail" define_cli_auth_test!(
); test_delete_gear_requires_authentication,
} &["gear", "delete", "--id", "123"]
);
define_cli_list_test!(
test_list_gear_works_without_authentication,
&["gear", "list"]
);
#[test] #[test]
fn test_add_gear_with_authentication() { fn test_add_gear_with_authentication() {
@ -52,18 +55,6 @@ fn test_add_gear_with_authentication() {
assert!(gear["id"].is_i64()); assert!(gear["id"].is_i64());
} }
#[test]
fn test_list_gear_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["gear", "list"], &[]);
assert!(
output.status.success(),
"gear list should work without auth"
);
}
#[test] #[test]
fn test_list_gear_shows_added_gear() { fn test_list_gear_shows_added_gear() {
let token = create_token("test-list-gear"); let token = create_token("test-list-gear");
@ -179,18 +170,6 @@ fn test_get_gear_by_id() {
assert_eq!(retrieved_gear["model"], "EG-1"); assert_eq!(retrieved_gear["model"], "EG-1");
} }
#[test]
fn test_update_gear_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["gear", "update", "--id", "123", "--make", "Updated"], &[]);
assert!(
!output.status.success(),
"gear update without auth should fail"
);
}
#[test] #[test]
fn test_update_gear_with_authentication() { fn test_update_gear_with_authentication() {
let token = create_token("test-update-gear"); let token = create_token("test-update-gear");
@ -224,18 +203,6 @@ fn test_update_gear_with_authentication() {
assert_eq!(updated_gear["model"], "Mini II"); assert_eq!(updated_gear["model"], "Mini II");
} }
#[test]
fn test_delete_gear_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["gear", "delete", "--id", "123"], &[]);
assert!(
!output.status.success(),
"gear delete without auth should fail"
);
}
#[test] #[test]
fn test_delete_gear_with_authentication() { fn test_delete_gear_with_authentication() {
let token = create_token("test-delete-gear"); let token = create_token("test-delete-gear");

View file

@ -209,32 +209,33 @@ pub fn run_brewlog(args: &[&str], env: &[(&str, &str)]) -> std::process::Output
cmd.output().expect("Failed to run brewlog command") cmd.output().expect("Failed to run brewlog command")
} }
/// Helper to create a roaster and return its ID /// Generic helper: run a CLI command with auth, assert success, parse JSON, extract ID.
pub fn create_roaster(name: &str, token: &str) -> String { fn create_entity_cli(args: &[&str], token: &str, label: &str) -> String {
let output = run_brewlog( let output = run_brewlog(args, &[("BREWLOG_TOKEN", token)]);
&["roaster", "add", "--name", name, "--country", "UK"],
&[("BREWLOG_TOKEN", token)],
);
if !output.status.success() { if !output.status.success() {
panic!( panic!(
"Failed to create roaster: {}", "Failed to create {label}: {}",
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
); );
} }
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
let roaster: serde_json::Value = let value: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
serde_json::from_str(&stdout).expect("Should output valid JSON"); value["id"]
roaster["id"]
.as_i64() .as_i64()
.expect("roaster id should be numeric") .unwrap_or_else(|| panic!("{label} id should be numeric"))
.to_string() .to_string()
} }
/// Helper to create a roast and return its ID pub fn create_roaster(name: &str, token: &str) -> String {
create_entity_cli(
&["roaster", "add", "--name", name, "--country", "UK"],
token,
"roaster",
)
}
pub fn create_roast(roaster_id: &str, name: &str, token: &str) -> String { pub fn create_roast(roaster_id: &str, name: &str, token: &str) -> String {
let output = run_brewlog( create_entity_cli(
&[ &[
"roast", "roast",
"add", "add",
@ -253,25 +254,11 @@ pub fn create_roast(roaster_id: &str, name: &str, token: &str) -> String {
"--tasting-notes", "--tasting-notes",
"Blackcurrant", "Blackcurrant",
], ],
&[("BREWLOG_TOKEN", token)], token,
); "roast",
)
if !output.status.success() {
panic!(
"Failed to create roast: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let roast: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
roast["id"]
.as_i64()
.expect("roast id should be numeric")
.to_string()
} }
/// Helper to create a cafe and return its ID
pub fn create_cafe( pub fn create_cafe(
name: &str, name: &str,
city: &str, city: &str,
@ -280,7 +267,7 @@ pub fn create_cafe(
longitude: &str, longitude: &str,
token: &str, token: &str,
) -> String { ) -> String {
let output = run_brewlog( create_entity_cli(
&[ &[
"cafe", "cafe",
"add", "add",
@ -295,20 +282,32 @@ pub fn create_cafe(
"--longitude", "--longitude",
longitude, longitude,
], ],
&[("BREWLOG_TOKEN", token)], token,
); "cafe",
)
if !output.status.success() { }
panic!(
"Failed to create cafe: {}", pub fn create_bag(roast_id: &str, token: &str) -> String {
String::from_utf8_lossy(&output.stderr) create_entity_cli(
); &["bag", "add", "--roast-id", roast_id, "--amount", "250"],
} token,
"bag",
let stdout = String::from_utf8_lossy(&output.stdout); )
let cafe: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON"); }
cafe["id"]
.as_i64() pub fn create_gear(category: &str, make: &str, model: &str, token: &str) -> String {
.expect("cafe id should be numeric") create_entity_cli(
.to_string() &[
"gear",
"add",
"--category",
category,
"--make",
make,
"--model",
model,
],
token,
"gear",
)
} }

View file

@ -6,4 +6,5 @@ pub mod gear_cli;
pub mod helpers; pub mod helpers;
pub mod roasters_cli; pub mod roasters_cli;
pub mod roasts_cli; pub mod roasts_cli;
pub mod test_macros;
pub mod tokens_cli; pub mod tokens_cli;

View file

@ -1,27 +1,37 @@
use crate::helpers::{create_roaster, create_token, run_brewlog, server_info}; use crate::helpers::{create_roaster, create_token, run_brewlog};
use crate::test_macros::{define_cli_auth_test, define_cli_list_test};
use serde_json::Value; use serde_json::Value;
#[test] define_cli_auth_test!(
fn test_add_roaster_requires_authentication() { test_add_roaster_requires_authentication,
let _ = server_info(); // Ensure server is started
let output = run_brewlog(
&[ &[
"roaster", "roaster",
"add", "add",
"--name", "--name",
"Test Roasters", "Test Roasters",
"--country", "--country",
"UK", "UK"
], ]
&[], );
); define_cli_auth_test!(
test_delete_roaster_requires_authentication,
assert!( &["roaster", "delete", "--id", "some-id"]
!output.status.success(), );
"roaster add without auth should fail" define_cli_auth_test!(
); test_update_roaster_requires_authentication,
} &[
"roaster",
"update",
"--id",
"some-id",
"--name",
"Updated Name"
]
);
define_cli_list_test!(
test_list_roasters_works_without_authentication,
&["roaster", "list"]
);
#[test] #[test]
fn test_add_roaster_with_authentication() { fn test_add_roaster_with_authentication() {
@ -54,23 +64,6 @@ fn test_add_roaster_with_authentication() {
assert!(roaster["id"].is_i64(), "Should have an ID"); assert!(roaster["id"].is_i64(), "Should have an ID");
} }
#[test]
fn test_list_roasters_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["roaster", "list"], &[]);
assert!(
output.status.success(),
"roaster list should work without auth"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let roasters: Value = serde_json::from_str(&stdout).expect("Should output valid JSON array");
assert!(roasters.is_array(), "Should return an array");
}
#[test] #[test]
fn test_list_roasters_shows_added_roaster() { fn test_list_roasters_shows_added_roaster() {
let token = create_token("test-list-roasters"); let token = create_token("test-list-roasters");
@ -96,37 +89,3 @@ fn test_list_roasters_shows_added_roaster() {
.any(|r| r["id"].as_i64().unwrap().to_string() == roaster_id); .any(|r| r["id"].as_i64().unwrap().to_string() == roaster_id);
assert!(found, "Should find the added roaster in the list"); assert!(found, "Should find the added roaster in the list");
} }
#[test]
fn test_delete_roaster_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["roaster", "delete", "--id", "some-id"], &[]);
assert!(
!output.status.success(),
"roaster delete without auth should fail"
);
}
#[test]
fn test_update_roaster_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&[
"roaster",
"update",
"--id",
"some-id",
"--name",
"Updated Name",
],
&[],
);
assert!(
!output.status.success(),
"roaster update without auth should fail"
);
}

View file

@ -1,11 +1,9 @@
use crate::helpers::{create_roast, create_roaster, create_token, run_brewlog, server_info}; use crate::helpers::{create_roast, create_roaster, create_token, run_brewlog};
use crate::test_macros::{define_cli_auth_test, define_cli_list_test};
use serde_json::Value; use serde_json::Value;
#[test] define_cli_auth_test!(
fn test_add_roast_requires_authentication() { test_add_roast_requires_authentication,
let _ = server_info();
let output = run_brewlog(
&[ &[
"roast", "roast",
"add", "add",
@ -20,16 +18,21 @@ fn test_add_roast_requires_authentication() {
"--producer", "--producer",
"Local Coop", "Local Coop",
"--process", "--process",
"Washed", "Washed"
], ]
&[], );
); define_cli_auth_test!(
test_update_roast_requires_authentication,
assert!( &["roast", "update", "--id", "123", "--name", "Updated"]
!output.status.success(), );
"roast add without auth should fail" define_cli_auth_test!(
); test_delete_roast_requires_authentication,
} &["roast", "delete", "--id", "some-id"]
);
define_cli_list_test!(
test_list_roasts_works_without_authentication,
&["roast", "list"]
);
#[test] #[test]
fn test_add_roast_with_authentication() { fn test_add_roast_with_authentication() {
@ -78,23 +81,6 @@ fn test_add_roast_with_authentication() {
assert!(roast["id"].is_i64(), "Should have an ID"); assert!(roast["id"].is_i64(), "Should have an ID");
} }
#[test]
fn test_list_roasts_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["roast", "list"], &[]);
assert!(
output.status.success(),
"roast list should work without auth"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let roasts: Value = serde_json::from_str(&stdout).expect("Should output valid JSON array");
assert!(roasts.is_array(), "Should return an array");
}
#[test] #[test]
fn test_list_roasts_shows_added_roast() { fn test_list_roasts_shows_added_roast() {
let token = create_token("test-list-roasts"); let token = create_token("test-list-roasts");
@ -156,21 +142,6 @@ fn test_list_roasts_shows_added_roast() {
); );
} }
#[test]
fn test_update_roast_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&["roast", "update", "--id", "123", "--name", "Updated"],
&[],
);
assert!(
!output.status.success(),
"roast update without auth should fail"
);
}
#[test] #[test]
fn test_update_roast_with_authentication() { fn test_update_roast_with_authentication() {
let token = create_token("test-update-roast"); let token = create_token("test-update-roast");
@ -196,15 +167,3 @@ fn test_update_roast_with_authentication() {
let updated_roast: Value = serde_json::from_slice(&output.stdout).unwrap(); let updated_roast: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(updated_roast["name"], "Updated Name"); assert_eq!(updated_roast["name"], "Updated Name");
} }
#[test]
fn test_delete_roast_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["roast", "delete", "--id", "some-id"], &[]);
assert!(
!output.status.success(),
"roast delete without auth should fail"
);
}

40
tests/cli/test_macros.rs Normal file
View file

@ -0,0 +1,40 @@
/// Generates a test that asserts a CLI command fails without authentication.
macro_rules! define_cli_auth_test {
($name:ident, $args:expr) => {
#[test]
fn $name() {
let _ = crate::helpers::server_info();
let output = crate::helpers::run_brewlog($args, &[]);
assert!(
!output.status.success(),
"{} should require authentication",
stringify!($name)
);
}
};
}
/// Generates a test that asserts a CLI list command succeeds without
/// authentication and returns a valid JSON array.
macro_rules! define_cli_list_test {
($name:ident, $args:expr) => {
#[test]
fn $name() {
let _ = crate::helpers::server_info();
let output = crate::helpers::run_brewlog($args, &[]);
assert!(
output.status.success(),
"{} should succeed: {}",
stringify!($name),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let items: serde_json::Value =
serde_json::from_str(&stdout).expect("Should output valid JSON");
assert!(items.is_array(), "Should return a JSON array");
}
};
}
pub(crate) use define_cli_auth_test;
pub(crate) use define_cli_list_test;

View file

@ -1,16 +1,11 @@
use crate::helpers::{create_token, run_brewlog, server_info}; use crate::helpers::{create_token, run_brewlog};
use crate::test_macros::define_cli_auth_test;
#[test] define_cli_auth_test!(test_list_tokens_requires_authentication, &["token", "list"]);
fn test_list_tokens_requires_authentication() { define_cli_auth_test!(
let _ = server_info(); test_revoke_token_requires_authentication,
&["token", "revoke", "--id", "1"]
let output = run_brewlog(&["token", "list"], &[]); );
assert!(
!output.status.success(),
"token list without auth should fail"
);
}
#[test] #[test]
fn test_list_tokens_with_authentication() { fn test_list_tokens_with_authentication() {
@ -30,18 +25,6 @@ fn test_list_tokens_with_authentication() {
); );
} }
#[test]
fn test_revoke_token_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["token", "revoke", "--id", "1"], &[]);
assert!(
!output.status.success(),
"token revoke without auth should fail"
);
}
#[test] #[test]
fn test_revoke_token_with_authentication() { fn test_revoke_token_with_authentication() {
let token = create_token("test-revoke-token"); let token = create_token("test-revoke-token");