brewlog/tests/server/roasters_api.rs
Jon Seager ad4ee2617a
refactor(tests): add generic server helpers and CRUD test macros
- Add `paste` dev-dependency for macro identifier concatenation
- Add `create_entity<P, R>()` generic helper, convert per-entity
  creation helpers to thin wrappers
- Add `define_crud_tests!` macro generating nonexistent-GET/DELETE-404,
  empty-list-200, malformed-JSON-400, and missing-fields-400 tests
- Apply macro to roasters, cafes, cups, and roasts API tests
2026-02-06 14:26:54 +00:00

372 lines
10 KiB
Rust

use crate::helpers::spawn_app_with_auth;
use crate::test_macros::define_crud_tests;
use brewlog::domain::roasters::{NewRoaster, Roaster, UpdateRoaster};
define_crud_tests!(
entity: roaster,
path: "/roasters",
list_type: Roaster,
malformed_json: r#"{"name": "Test", "country": }"#,
missing_fields: r#"{"name": "Test Roasters"}"#
);
#[tokio::test]
async fn creating_a_roaster_returns_a_201_for_valid_data() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
name: "Test Roasters".to_string(),
country: "United Kingdom".to_string(),
city: Some("London".to_string()),
homepage: Some("https://example.com".to_string()),
};
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 201);
let roaster: Roaster = response.json().await.expect("Failed to parse response");
assert_eq!(roaster.name, "Test Roasters");
assert_eq!(roaster.country, "United Kingdom");
assert_eq!(roaster.city, Some("London".to_string()));
assert_eq!(roaster.homepage, Some("https://example.com".to_string()));
}
#[tokio::test]
async fn creating_a_roaster_persists_the_data() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
name: "Persistent Roasters".to_string(),
country: "France".to_string(),
city: Some("Paris".to_string()),
homepage: None,
};
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to execute request");
let roaster: Roaster = response.json().await.expect("Failed to parse response");
// Assert - Fetch the roaster to verify it was persisted
let fetched_roaster = app
.roaster_repo
.get(roaster.id.clone())
.await
.expect("Failed to fetch roaster");
assert_eq!(fetched_roaster.name, "Persistent Roasters");
assert_eq!(fetched_roaster.country, "France");
assert_eq!(fetched_roaster.city, Some("Paris".to_string()));
}
#[tokio::test]
async fn getting_a_roaster_returns_a_200_for_valid_id() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
name: "Fetchable Roasters".to_string(),
country: "Germany".to_string(),
city: Some("Berlin".to_string()),
homepage: None,
};
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to create roaster");
let created_roaster: Roaster = create_response
.json()
.await
.expect("Failed to parse response");
// Act
let response = client
.get(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roaster: Roaster = response.json().await.expect("Failed to parse response");
assert_eq!(roaster.id, created_roaster.id);
assert_eq!(roaster.name, "Fetchable Roasters");
}
#[tokio::test]
async fn listing_roasters_returns_a_200_with_multiple_roasters() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// Create multiple roasters
let roaster1 = NewRoaster {
name: "First Roasters".to_string(),
country: "UK".to_string(),
city: None,
homepage: None,
};
let roaster2 = NewRoaster {
name: "Second Roasters".to_string(),
country: "USA".to_string(),
city: Some("New York".to_string()),
homepage: None,
};
client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roaster1)
.send()
.await
.expect("Failed to create first roaster");
client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&roaster2)
.send()
.await
.expect("Failed to create second roaster");
// Act
let response = client
.get(app.api_url("/roasters"))
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let roasters: Vec<Roaster> = response.json().await.expect("Failed to parse response");
assert_eq!(roasters.len(), 2);
}
#[tokio::test]
async fn updating_a_roaster_returns_a_200_for_valid_data() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
name: "Original Name".to_string(),
country: "UK".to_string(),
city: Some("Manchester".to_string()),
homepage: None,
};
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to create roaster");
let created_roaster: Roaster = create_response
.json()
.await
.expect("Failed to parse response");
let update = UpdateRoaster {
name: Some("Updated Name".to_string()),
country: None,
city: Some("Liverpool".to_string()),
homepage: Some("https://updated.com".to_string()),
};
// Act
let response = client
.put(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 200);
let updated_roaster: Roaster = response.json().await.expect("Failed to parse response");
assert_eq!(updated_roaster.name, "Updated Name");
assert_eq!(updated_roaster.country, "UK"); // Should remain unchanged
assert_eq!(updated_roaster.city, Some("Liverpool".to_string()));
assert_eq!(
updated_roaster.homepage,
Some("https://updated.com".to_string())
);
}
#[tokio::test]
async fn updating_a_roaster_with_no_changes_returns_a_400() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
name: "Test Roaster".to_string(),
country: "UK".to_string(),
city: None,
homepage: None,
};
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to create roaster");
let created_roaster: Roaster = create_response
.json()
.await
.expect("Failed to parse response");
let update = UpdateRoaster {
name: None,
country: None,
city: None,
homepage: None,
};
// Act
let response = client
.put(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn updating_a_nonexistent_roaster_returns_a_404() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let update = UpdateRoaster {
name: Some("New Name".to_string()),
country: None,
city: None,
homepage: None,
};
// Act
let response = client
.put(app.api_url("/roasters/999999"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 404);
}
#[tokio::test]
async fn deleting_a_roaster_returns_a_204_for_valid_id() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let new_roaster = NewRoaster {
name: "To Be Deleted".to_string(),
country: "UK".to_string(),
city: None,
homepage: None,
};
let create_response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to create roaster");
let created_roaster: Roaster = create_response
.json()
.await
.expect("Failed to parse response");
// Act
let response = client
.delete(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
// Assert
assert_eq!(response.status(), 204);
// Verify roaster was deleted
let get_response = client
.get(app.api_url(&format!("/roasters/{}", created_roaster.id)))
.send()
.await
.expect("Failed to execute request");
assert_eq!(get_response.status(), 404);
}
#[tokio::test]
async fn creating_a_roaster_with_empty_name_returns_a_201_after_normalization() {
// Arrange
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
// The normalize function trims whitespace, so empty/whitespace names become empty
let new_roaster = NewRoaster {
name: " ".to_string(),
country: "UK".to_string(),
city: None,
homepage: None,
};
// Act
let response = client
.post(app.api_url("/roasters"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_roaster)
.send()
.await
.expect("Failed to execute request");
// Assert - The API accepts this but normalizes to empty string
assert_eq!(response.status(), 201);
}