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 crate::test_macros::define_cli_auth_test;
define_cli_auth_test!(backup_requires_auth, &["backup"]);
#[test]
fn backup_produces_valid_json() {
@ -25,13 +28,3 @@ fn backup_produces_valid_json() {
assert!(data["cafes"].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;
#[test]
fn test_add_bag_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&["bag", "add", "--roast-id", "123", "--amount", "250.0"],
&[],
);
assert!(!output.status.success(), "bag add without auth should fail");
}
define_cli_auth_test!(
test_add_bag_requires_authentication,
&["bag", "add", "--roast-id", "123", "--amount", "250.0"]
);
define_cli_auth_test!(
test_update_bag_requires_authentication,
&["bag", "update", "--id", "123", "--remaining", "100.0"]
);
define_cli_auth_test!(
test_delete_bag_requires_authentication,
&["bag", "delete", "--id", "123"]
);
define_cli_list_test!(
test_list_bags_works_without_authentication,
&["bag", "list"]
);
#[test]
fn test_add_bag_with_authentication() {
@ -44,21 +50,6 @@ fn test_add_bag_with_authentication() {
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]
fn test_update_bag_with_authentication() {
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));
}
#[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]
fn test_list_bags_shows_added_bag() {
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]
fn test_delete_bag_with_authentication() {
let token = create_token("test-delete-bag");

View file

@ -1,55 +1,6 @@
use crate::helpers::{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()
}
use crate::helpers::{
create_bag, create_gear, create_roast, create_roaster, create_token, run_brewlog,
};
#[test]
fn brew_add_creates_brew_via_api() {

View file

@ -1,33 +1,43 @@
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;
#[test]
fn test_add_cafe_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&[
"cafe",
"add",
"--name",
"Test Cafe",
"--city",
"London",
"--country",
"UK",
"--latitude",
"51.5074",
"--longitude",
"-0.1278",
],
&[],
);
assert!(
!output.status.success(),
"cafe add without auth should fail"
);
}
define_cli_auth_test!(
test_add_cafe_requires_authentication,
&[
"cafe",
"add",
"--name",
"Test Cafe",
"--city",
"London",
"--country",
"UK",
"--latitude",
"51.5074",
"--longitude",
"-0.1278"
]
);
define_cli_auth_test!(
test_delete_cafe_requires_authentication,
&["cafe", "delete", "--id", "some-id"]
);
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]
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");
}
#[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]
fn test_list_cafes_shows_added_cafe() {
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);
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,29 +1,32 @@
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;
#[test]
fn test_add_gear_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&[
"gear",
"add",
"--category",
"grinder",
"--make",
"Baratza",
"--model",
"Encore",
],
&[],
);
assert!(
!output.status.success(),
"gear add without auth should fail"
);
}
define_cli_auth_test!(
test_add_gear_requires_authentication,
&[
"gear",
"add",
"--category",
"grinder",
"--make",
"Baratza",
"--model",
"Encore"
]
);
define_cli_auth_test!(
test_update_gear_requires_authentication,
&["gear", "update", "--id", "123", "--make", "Updated"]
);
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]
fn test_add_gear_with_authentication() {
@ -52,18 +55,6 @@ fn test_add_gear_with_authentication() {
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]
fn test_list_gear_shows_added_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");
}
#[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]
fn test_update_gear_with_authentication() {
let token = create_token("test-update-gear");
@ -224,18 +203,6 @@ fn test_update_gear_with_authentication() {
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]
fn test_delete_gear_with_authentication() {
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")
}
/// Helper to create a roaster and return its ID
pub fn create_roaster(name: &str, token: &str) -> String {
let output = run_brewlog(
&["roaster", "add", "--name", name, "--country", "UK"],
&[("BREWLOG_TOKEN", token)],
);
/// Generic helper: run a CLI command with auth, assert success, parse JSON, extract ID.
fn create_entity_cli(args: &[&str], token: &str, label: &str) -> String {
let output = run_brewlog(args, &[("BREWLOG_TOKEN", token)]);
if !output.status.success() {
panic!(
"Failed to create roaster: {}",
"Failed to create {label}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let roaster: serde_json::Value =
serde_json::from_str(&stdout).expect("Should output valid JSON");
roaster["id"]
let value: serde_json::Value = serde_json::from_str(&stdout).expect("Should output valid JSON");
value["id"]
.as_i64()
.expect("roaster id should be numeric")
.unwrap_or_else(|| panic!("{label} id should be numeric"))
.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 {
let output = run_brewlog(
create_entity_cli(
&[
"roast",
"add",
@ -253,25 +254,11 @@ pub fn create_roast(roaster_id: &str, name: &str, token: &str) -> String {
"--tasting-notes",
"Blackcurrant",
],
&[("BREWLOG_TOKEN", token)],
);
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()
token,
"roast",
)
}
/// Helper to create a cafe and return its ID
pub fn create_cafe(
name: &str,
city: &str,
@ -280,7 +267,7 @@ pub fn create_cafe(
longitude: &str,
token: &str,
) -> String {
let output = run_brewlog(
create_entity_cli(
&[
"cafe",
"add",
@ -295,20 +282,32 @@ pub fn create_cafe(
"--longitude",
longitude,
],
&[("BREWLOG_TOKEN", token)],
);
if !output.status.success() {
panic!(
"Failed to create cafe: {}",
String::from_utf8_lossy(&output.stderr)
);
}
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()
.expect("cafe id should be numeric")
.to_string()
token,
"cafe",
)
}
pub fn create_bag(roast_id: &str, token: &str) -> String {
create_entity_cli(
&["bag", "add", "--roast-id", roast_id, "--amount", "250"],
token,
"bag",
)
}
pub fn create_gear(category: &str, make: &str, model: &str, token: &str) -> String {
create_entity_cli(
&[
"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 roasters_cli;
pub mod roasts_cli;
pub mod test_macros;
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;
#[test]
fn test_add_roaster_requires_authentication() {
let _ = server_info(); // Ensure server is started
let output = run_brewlog(
&[
"roaster",
"add",
"--name",
"Test Roasters",
"--country",
"UK",
],
&[],
);
assert!(
!output.status.success(),
"roaster add without auth should fail"
);
}
define_cli_auth_test!(
test_add_roaster_requires_authentication,
&[
"roaster",
"add",
"--name",
"Test Roasters",
"--country",
"UK"
]
);
define_cli_auth_test!(
test_delete_roaster_requires_authentication,
&["roaster", "delete", "--id", "some-id"]
);
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]
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");
}
#[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]
fn test_list_roasters_shows_added_roaster() {
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);
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,35 +1,38 @@
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;
#[test]
fn test_add_roast_requires_authentication() {
let _ = server_info();
let output = run_brewlog(
&[
"roast",
"add",
"--roaster-id",
"some-id",
"--name",
"Test Roast",
"--origin",
"Ethiopia",
"--region",
"Yirgacheffe",
"--producer",
"Local Coop",
"--process",
"Washed",
],
&[],
);
assert!(
!output.status.success(),
"roast add without auth should fail"
);
}
define_cli_auth_test!(
test_add_roast_requires_authentication,
&[
"roast",
"add",
"--roaster-id",
"some-id",
"--name",
"Test Roast",
"--origin",
"Ethiopia",
"--region",
"Yirgacheffe",
"--producer",
"Local Coop",
"--process",
"Washed"
]
);
define_cli_auth_test!(
test_update_roast_requires_authentication,
&["roast", "update", "--id", "123", "--name", "Updated"]
);
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]
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");
}
#[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]
fn test_list_roasts_shows_added_roast() {
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]
fn test_update_roast_with_authentication() {
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();
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]
fn test_list_tokens_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["token", "list"], &[]);
assert!(
!output.status.success(),
"token list without auth should fail"
);
}
define_cli_auth_test!(test_list_tokens_requires_authentication, &["token", "list"]);
define_cli_auth_test!(
test_revoke_token_requires_authentication,
&["token", "revoke", "--id", "1"]
);
#[test]
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]
fn test_revoke_token_with_authentication() {
let token = create_token("test-revoke-token");