CI / test (push) Canceled after 0s
Covers Clients, Invoices, Quotes, Credits, Payments, Products, Expenses, Projects, Tasks, and Vendors, each with list, list_all, get, create, update, and delete, generated by the resource_methods! macro. Every model carries a #[serde(flatten)] extra map so fields the API returns but the crate does not model survive a get/update round-trip instead of being silently dropped. Includes a Nix dev shell (Rust via rust-overlay), a GitHub Actions CI workflow running build/test/fmt/clippy, and round-trip tests for the extra-preservation invariant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
4.6 KiB
Rust
153 lines
4.6 KiB
Rust
use invoiceninjars::models::{
|
|
Client, Credit, Expense, Invoice, ListParams, Project, Quote, Task, Vendor,
|
|
};
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn client_round_trip_preserves_unknown_fields() {
|
|
let payload = json!({
|
|
"id": "abc123",
|
|
"name": "Acme Inc",
|
|
"custom_value1": "unmodeled field",
|
|
});
|
|
|
|
let client: Client = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(client.name, "Acme Inc");
|
|
assert_eq!(
|
|
client.extra.get("custom_value1").unwrap(),
|
|
"unmodeled field"
|
|
);
|
|
|
|
let round_tripped = serde_json::to_value(&client).unwrap();
|
|
assert_eq!(round_tripped["custom_value1"], "unmodeled field");
|
|
}
|
|
|
|
#[test]
|
|
fn invoice_round_trip_preserves_unknown_fields() {
|
|
let payload = json!({
|
|
"id": "inv1",
|
|
"client_id": "abc123",
|
|
"number": "INV-0001",
|
|
"custom_surcharge1": 12.5,
|
|
});
|
|
|
|
let invoice: Invoice = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(invoice.number, "INV-0001");
|
|
assert_eq!(invoice.extra.get("custom_surcharge1").unwrap(), 12.5);
|
|
|
|
let round_tripped = serde_json::to_value(&invoice).unwrap();
|
|
assert_eq!(round_tripped["custom_surcharge1"], 12.5);
|
|
}
|
|
|
|
#[test]
|
|
fn list_params_omits_unset_fields() {
|
|
let params = ListParams::new().per_page(25).sort("created_at|desc");
|
|
|
|
assert_eq!(
|
|
serde_json::to_value(¶ms).unwrap(),
|
|
json!({"per_page": 25, "sort": "created_at|desc"})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vendor_round_trip_preserves_nested_contact_fields() {
|
|
let payload = json!({
|
|
"id": "ven1",
|
|
"name": "Supply Co",
|
|
"custom_value2": "unmodeled vendor field",
|
|
"contacts": [{
|
|
"id": "vc1",
|
|
"first_name": "Ada",
|
|
"email": "ada@supply.example",
|
|
"is_primary": true,
|
|
"custom_value1": "unmodeled contact field",
|
|
}],
|
|
});
|
|
|
|
let vendor: Vendor = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(vendor.name, "Supply Co");
|
|
assert_eq!(vendor.contacts.len(), 1);
|
|
assert_eq!(vendor.contacts[0].first_name, "Ada");
|
|
assert!(vendor.contacts[0].is_primary);
|
|
|
|
let round_tripped = serde_json::to_value(&vendor).unwrap();
|
|
assert_eq!(round_tripped["custom_value2"], "unmodeled vendor field");
|
|
assert_eq!(
|
|
round_tripped["contacts"][0]["custom_value1"],
|
|
"unmodeled contact field"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn expense_round_trip_preserves_unknown_fields() {
|
|
let payload = json!({
|
|
"id": "exp1",
|
|
"vendor_id": "ven1",
|
|
"amount": 42.75,
|
|
"should_be_invoiced": true,
|
|
"custom_value3": "unmodeled field",
|
|
});
|
|
|
|
let expense: Expense = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(expense.vendor_id, "ven1");
|
|
assert_eq!(expense.amount, 42.75);
|
|
assert!(expense.should_be_invoiced);
|
|
|
|
let round_tripped = serde_json::to_value(&expense).unwrap();
|
|
assert_eq!(round_tripped["custom_value3"], "unmodeled field");
|
|
}
|
|
|
|
#[test]
|
|
fn task_round_trip_preserves_unknown_fields() {
|
|
let payload = json!({
|
|
"id": "task1",
|
|
"project_id": "proj1",
|
|
"description": "Design review",
|
|
"time_log": "[[1700000000,1700003600]]",
|
|
"custom_value1": "unmodeled field",
|
|
});
|
|
|
|
let task: Task = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(task.project_id, "proj1");
|
|
assert_eq!(task.time_log, "[[1700000000,1700003600]]");
|
|
|
|
let round_tripped = serde_json::to_value(&task).unwrap();
|
|
assert_eq!(round_tripped["custom_value1"], "unmodeled field");
|
|
}
|
|
|
|
#[test]
|
|
fn project_round_trip_preserves_unknown_fields() {
|
|
let payload = json!({
|
|
"id": "proj1",
|
|
"client_id": "abc123",
|
|
"name": "Website rebuild",
|
|
"budgeted_hours": 120.0,
|
|
"custom_value4": "unmodeled field",
|
|
});
|
|
|
|
let project: Project = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(project.name, "Website rebuild");
|
|
assert_eq!(project.budgeted_hours, 120.0);
|
|
|
|
let round_tripped = serde_json::to_value(&project).unwrap();
|
|
assert_eq!(round_tripped["custom_value4"], "unmodeled field");
|
|
}
|
|
|
|
/// `Quote` and `Credit` are type aliases for `Invoice`; this pins that the
|
|
/// aliased endpoints decode the shared invoice shape.
|
|
#[test]
|
|
fn quote_and_credit_share_the_invoice_shape() {
|
|
let payload = json!({
|
|
"id": "q1",
|
|
"number": "QUO-0001",
|
|
"entity_type": "quote",
|
|
});
|
|
|
|
let quote: Quote = serde_json::from_value(payload.clone()).unwrap();
|
|
assert_eq!(quote.number, "QUO-0001");
|
|
assert_eq!(quote.extra.get("entity_type").unwrap(), "quote");
|
|
|
|
let credit: Credit = serde_json::from_value(payload).unwrap();
|
|
assert_eq!(credit.number, "QUO-0001");
|
|
}
|