feat(roast): add update command for consistency with other entities
- Add PUT /api/v1/roasts/:id route handler - Add update() method to roasts HTTP client - Add UpdateRoastCommand to CLI with optional fields - Add CLI tests for roast update authentication and functionality Brings roast entity in line with roaster, bag, and gear which all support add/list/get/update/delete operations.
This commit is contained in:
parent
0090c4ba43
commit
00477bdbc0
5 changed files with 137 additions and 5 deletions
|
|
@ -41,7 +41,9 @@ pub fn app_router(state: AppState) -> axum::Router {
|
|||
)
|
||||
.route(
|
||||
"/roasts/:id",
|
||||
get(roasts::get_roast).delete(roasts::delete_roast),
|
||||
get(roasts::get_roast)
|
||||
.put(roasts::update_roast)
|
||||
.delete(roasts::delete_roast),
|
||||
)
|
||||
.route("/bags", get(bags::list_bags).post(bags::create_bag))
|
||||
.route(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::domain::bags::{BagFilter, BagSortKey};
|
|||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::domain::roasters::RoasterSortKey;
|
||||
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster};
|
||||
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
||||
use crate::presentation::web::templates::{
|
||||
RoastDetailTemplate, RoastListTemplate, RoastOptionsTemplate, RoastsTemplate,
|
||||
};
|
||||
|
|
@ -227,6 +227,40 @@ define_delete_handler!(
|
|||
render_roast_list_fragment
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user))]
|
||||
pub(crate) async fn update_roast(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
Path(id): Path<RoastId>,
|
||||
Json(payload): Json<UpdateRoast>,
|
||||
) -> Result<Json<RoastWithRoaster>, ApiError> {
|
||||
let has_changes = payload.roaster_id.is_some()
|
||||
|| payload.name.is_some()
|
||||
|| payload.origin.is_some()
|
||||
|| payload.region.is_some()
|
||||
|| payload.producer.is_some()
|
||||
|| payload.tasting_notes.is_some()
|
||||
|| payload.process.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
|
||||
state
|
||||
.roast_repo
|
||||
.update(id, payload)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let enriched = state
|
||||
.roast_repo
|
||||
.get_with_roaster(id)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
Ok(Json(enriched))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RoastsQuery {
|
||||
pub roaster_id: Option<String>,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
|||
use reqwest::StatusCode;
|
||||
|
||||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
use crate::domain::roasts::{NewRoast, RoastWithRoaster};
|
||||
use crate::domain::roasts::{NewRoast, RoastWithRoaster, UpdateRoast};
|
||||
|
||||
use super::BrewlogClient;
|
||||
|
||||
|
|
@ -57,6 +57,19 @@ impl<'a> RoastsClient<'a> {
|
|||
self.inner.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: RoastId, payload: &UpdateRoast) -> Result<RoastWithRoaster> {
|
||||
let url = self.inner.endpoint(&format!("api/v1/roasts/{id}"))?;
|
||||
let response = self
|
||||
.inner
|
||||
.request(reqwest::Method::PUT, url)
|
||||
.json(payload)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to issue update roast request")?;
|
||||
|
||||
self.inner.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: RoastId) -> Result<()> {
|
||||
let url = self.inner.endpoint(&format!("api/v1/roasts/{id}"))?;
|
||||
let response = self
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use clap::{Args, Subcommand};
|
|||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::print_json;
|
||||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
use crate::domain::roasts::NewRoast;
|
||||
use crate::domain::roasts::{NewRoast, UpdateRoast};
|
||||
use crate::infrastructure::client::BrewlogClient;
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
@ -15,6 +15,8 @@ pub enum RoastCommands {
|
|||
List(ListRoastsCommand),
|
||||
/// Get a roast by ID
|
||||
Get(GetRoastCommand),
|
||||
/// Update a roast
|
||||
Update(UpdateRoastCommand),
|
||||
/// Delete a roast
|
||||
Delete(DeleteRoastCommand),
|
||||
}
|
||||
|
|
@ -24,6 +26,7 @@ pub async fn run(client: &BrewlogClient, cmd: RoastCommands) -> Result<()> {
|
|||
RoastCommands::Add(c) => add_roast(client, c).await,
|
||||
RoastCommands::List(c) => list_roasts(client, c).await,
|
||||
RoastCommands::Get(c) => get_roast(client, c).await,
|
||||
RoastCommands::Update(c) => update_roast(client, c).await,
|
||||
RoastCommands::Delete(c) => delete_roast(client, c).await,
|
||||
}
|
||||
}
|
||||
|
|
@ -76,4 +79,43 @@ pub async fn list_roasts(client: &BrewlogClient, command: ListRoastsCommand) ->
|
|||
}
|
||||
|
||||
define_get_command!(GetRoastCommand, get_roast, RoastId, roasts);
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct UpdateRoastCommand {
|
||||
#[arg(long)]
|
||||
pub id: i64,
|
||||
#[arg(long)]
|
||||
pub roaster_id: Option<i64>,
|
||||
#[arg(long)]
|
||||
pub name: Option<String>,
|
||||
#[arg(long)]
|
||||
pub origin: Option<String>,
|
||||
#[arg(long)]
|
||||
pub region: Option<String>,
|
||||
#[arg(long)]
|
||||
pub producer: Option<String>,
|
||||
#[arg(long)]
|
||||
pub process: Option<String>,
|
||||
#[arg(long = "tasting-notes")]
|
||||
pub tasting_notes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn update_roast(client: &BrewlogClient, command: UpdateRoastCommand) -> Result<()> {
|
||||
let payload = UpdateRoast {
|
||||
roaster_id: command.roaster_id.map(RoasterId::new),
|
||||
name: command.name,
|
||||
origin: command.origin,
|
||||
region: command.region,
|
||||
producer: command.producer,
|
||||
tasting_notes: command.tasting_notes,
|
||||
process: command.process,
|
||||
};
|
||||
|
||||
let roast = client
|
||||
.roasts()
|
||||
.update(RoastId::new(command.id), &payload)
|
||||
.await?;
|
||||
print_json(&roast)
|
||||
}
|
||||
|
||||
define_delete_command!(DeleteRoastCommand, delete_roast, RoastId, roasts, "roast");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::helpers::{create_roaster, create_token, run_brewlog, server_info};
|
||||
use crate::helpers::{create_roast, create_roaster, create_token, run_brewlog, server_info};
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
|
|
@ -156,6 +156,47 @@ 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");
|
||||
|
||||
// Setup: create roaster and roast
|
||||
let roaster_id = create_roaster("Update Roast Roaster", &token);
|
||||
let roast_id = create_roast(&roaster_id, "Original Name", &token);
|
||||
|
||||
// Update the roast
|
||||
let output = run_brewlog(
|
||||
&[
|
||||
"roast",
|
||||
"update",
|
||||
"--id",
|
||||
&roast_id,
|
||||
"--name",
|
||||
"Updated Name",
|
||||
],
|
||||
&[("BREWLOG_TOKEN", &token)],
|
||||
);
|
||||
|
||||
assert!(output.status.success());
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue