Pin the response shapes captured from a real server

Twice now a mock written from the upstream source passed while a real server
disagreed — the system-log timestamps and the `/subscriptions/steps` shape —
and `frequency_id` and several empty-string fields were wrong the same way.
Reading the source harder was not going to find the next one.

`tests/fixtures/*.json` hold the *shapes* of real responses: every key, with
its JSON type, and no values at all. `real_shapes.rs` decodes each one and
checks that a decode plus re-serialise loses no key, so a field the models
silently drop now fails a test. It also refuses a fixture holding any real
value, and refuses a fixture that no test reads.

`seed_sample_data.rs` fills a disposable instance with one row of each kind
and `capture_shapes.rs` captures the shapes back out with GET requests only,
so the set can be refreshed when a divergence turns up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 21:56:17 -04:00
co-authored by Claude Opus 5
parent 83270e8262
commit 46a6ea1322
35 changed files with 3267 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"data": [
{
"activity_type_id": "",
"client_id": "",
"company_id": "",
"contact_id": "",
"created_at": 0,
"credit_id": "",
"expense_id": "",
"id": "",
"invoice_id": "",
"ip": "",
"is_system": false,
"notes": "",
"payment_id": "",
"purchase_order_id": "",
"quote_id": "",
"recurring_expense_id": "",
"recurring_invoice_id": "",
"task_id": "",
"token_id": "",
"updated_at": 0,
"user_id": "",
"vendor_contact_id": "",
"vendor_id": ""
}
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"data": [
{
"activity_type_id": 0,
"created_at": 0,
"hashed_id": "",
"id": "",
"ip": "",
"notes": "",
"purchase_order": {
"hashed_id": "",
"label": ""
},
"user": {
"hashed_id": "",
"label": ""
},
"vendor": {
"hashed_id": "",
"label": ""
}
}
]
}
+114
View File
@@ -0,0 +1,114 @@
{
"data": [
{
"address1": "",
"address2": "",
"archived_at": 0,
"assigned_user_id": "",
"balance": 0,
"city": "",
"classification": "",
"client_hash": "",
"contacts": [
{
"archived_at": 0,
"can_sign": false,
"cc_only": false,
"contact_key": "",
"created_at": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"email": "",
"first_name": "",
"id": "",
"is_locked": false,
"is_primary": false,
"last_login": 0,
"last_name": "",
"link": "",
"password": "",
"phone": "",
"send_email": false,
"updated_at": 0
}
],
"country_id": "",
"created_at": 0,
"credit_balance": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"display_name": "",
"documents": [
{
"archived_at": 0,
"assigned_user_id": "",
"created_at": 0,
"disk": "",
"documentable_id": "",
"documentable_type": "",
"hash": "",
"height": 0,
"id": "",
"is_default": false,
"is_deleted": false,
"is_public": false,
"link": "",
"name": "",
"preview": "",
"project_id": "",
"size": 0,
"type": "",
"updated_at": 0,
"url": "",
"user_id": "",
"vendor_id": "",
"width": 0
}
],
"e_invoice": {},
"gateway_tokens": [],
"group_settings_id": "",
"has_valid_vat_number": false,
"id": "",
"id_number": "",
"industry_id": "",
"is_deleted": false,
"is_tax_exempt": false,
"last_login": 0,
"locations": [],
"name": "",
"number": "",
"paid_to_date": 0,
"payment_balance": 0,
"phone": "",
"postal_code": "",
"private_notes": "",
"public_notes": "",
"routing_id": "",
"settings": {
"currency_id": "",
"entity": "",
"industry_id": null,
"size_id": null
},
"shipping_address1": "",
"shipping_address2": "",
"shipping_city": "",
"shipping_country_id": "",
"shipping_postal_code": "",
"shipping_state": "",
"size_id": "",
"state": "",
"tags": [],
"tax_info": {},
"updated_at": 0,
"user_id": "",
"vat_number": "",
"website": ""
}
]
}
+1059
View File
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"backup": {
"adjustable_amount": 0,
"cancellation": {
"adjustment": 0,
"status_id": 0
},
"child_invoice_ids": [],
"document_type": null,
"e_invoice_cleared_at": null,
"e_invoice_status": null,
"guid": "",
"notes": null,
"parent_invoice_id": null,
"parent_invoice_number": null,
"redirect": null
},
"balance": 0,
"client_id": "",
"created_at": 0,
"custom_surcharge1": 0,
"custom_surcharge2": 0,
"custom_surcharge3": 0,
"custom_surcharge4": 0,
"custom_surcharge_tax1": false,
"custom_surcharge_tax2": false,
"custom_surcharge_tax3": false,
"custom_surcharge_tax4": false,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"design_id": "",
"discount": 0,
"documents": [],
"due_date": "",
"e_invoice": {},
"entity_type": "",
"exchange_rate": 0,
"footer": "",
"has_expenses": false,
"has_tasks": false,
"id": "",
"invitations": [
{
"archived_at": 0,
"can_sign": false,
"client_contact_id": "",
"created_at": 0,
"email_error": "",
"email_status": "",
"id": "",
"key": "",
"link": "",
"message_id": "",
"opened_date": "",
"sent_date": "",
"updated_at": 0,
"viewed_date": ""
}
],
"invoice_id": "",
"is_amount_discount": false,
"is_deleted": false,
"last_sent_date": "",
"line_items": [
{
"cost": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"discount": 0,
"expense_id": "",
"gross_line_total": 0,
"income_account_id": "",
"is_amount_discount": false,
"line_total": 0,
"net_cost": 0,
"notes": "",
"product_cost": 0,
"product_key": "",
"quantity": 0,
"sort_id": "",
"tags": "",
"task_id": "",
"tax_amount": 0,
"tax_id": "",
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"type_id": "",
"unit_code": ""
}
],
"location_id": "",
"next_send_date": "",
"number": "",
"paid_to_date": 0,
"partial": 0,
"partial_due_date": "",
"po_number": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"reminder1_sent": "",
"reminder2_sent": "",
"reminder3_sent": "",
"reminder_last_sent": "",
"status_id": "",
"subscription_id": "",
"sync": null,
"tags": [],
"tax_info": {},
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"terms": "",
"total_taxes": 0,
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"data": [
{
"archived_at": 0,
"created_at": 0,
"design": {
"body": "",
"footer": "",
"header": "",
"includes": "",
"product": "",
"task": ""
},
"entities": "",
"id": "",
"is_active": false,
"is_custom": false,
"is_deleted": false,
"is_free": false,
"is_template": false,
"name": "",
"updated_at": 0
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"data": [
{
"archived_at": 0,
"assigned_user_id": "",
"created_at": 0,
"disk": "",
"documentable_id": "",
"documentable_type": "",
"hash": "",
"height": 0,
"id": "",
"is_default": false,
"is_deleted": false,
"is_public": false,
"link": "",
"name": "",
"preview": "",
"project_id": "",
"size": 0,
"type": "",
"updated_at": 0,
"url": "",
"user_id": "",
"vendor_id": "",
"width": 0
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"data": [
{
"archived_at": 0,
"color": "",
"created_at": 0,
"id": "",
"is_deleted": false,
"name": "",
"updated_at": 0,
"user_id": ""
}
]
}
+55
View File
@@ -0,0 +1,55 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"bank_id": "",
"calculate_tax_by_amount": false,
"category_id": "",
"client_id": "",
"created_at": 0,
"currency_id": "",
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"documents": [],
"e_invoice": {},
"entity_type": "",
"exchange_rate": 0,
"expense_currency_id": "",
"foreign_amount": 0,
"id": "",
"invoice_currency_id": "",
"invoice_documents": false,
"invoice_id": "",
"is_deleted": false,
"number": "",
"payment_date": "",
"payment_type_id": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"recurring_expense_id": "",
"should_be_invoiced": false,
"tags": [],
"tax_amount1": 0,
"tax_amount2": 0,
"tax_amount3": 0,
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"transaction_id": "",
"transaction_reference": "",
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"data": [
{
"archived_at": 0,
"created_at": 0,
"documents": [],
"id": "",
"is_deleted": false,
"name": "",
"settings": {
"entity": ""
},
"updated_at": 0
}
]
}
+37
View File
@@ -0,0 +1,37 @@
{
"api_version": "",
"cache_enabled": false,
"env_writable": false,
"exchange_rate_api_not_configured": false,
"exec": false,
"extensions": [
{
"gd": false
}
],
"file_permissions": "",
"flutter_renderer": "",
"is_docker": false,
"jobs_pending": 0,
"mail_mailer": "",
"open_basedir": false,
"pdf_engine": "",
"pending_migrations": false,
"phantom_enabled": false,
"php_version": {
"current_php_cli_version": "",
"current_php_version": "",
"is_okay": false,
"memory_limit": "",
"minimum_php_version": ""
},
"queue": "",
"queue_data": {
"failed": 0,
"last_error": "",
"pending": 0
},
"simple_db_check": false,
"system_health": false,
"trailing_slash": false
}
+140
View File
@@ -0,0 +1,140 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"auto_bill_enabled": false,
"backup": {
"adjustable_amount": 0,
"cancellation": {
"adjustment": 0,
"status_id": 0
},
"child_invoice_ids": [],
"document_type": null,
"e_invoice_cleared_at": null,
"e_invoice_status": null,
"guid": "",
"notes": null,
"parent_invoice_id": null,
"parent_invoice_number": null,
"redirect": null
},
"balance": 0,
"client_id": "",
"created_at": 0,
"custom_surcharge1": 0,
"custom_surcharge2": 0,
"custom_surcharge3": 0,
"custom_surcharge4": 0,
"custom_surcharge_tax1": false,
"custom_surcharge_tax2": false,
"custom_surcharge_tax3": false,
"custom_surcharge_tax4": false,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"design_id": "",
"discount": 0,
"documents": [],
"due_date": "",
"e_invoice": {},
"entity_type": "",
"exchange_rate": 0,
"footer": "",
"has_expenses": false,
"has_tasks": false,
"id": "",
"invitations": [
{
"archived_at": 0,
"can_sign": false,
"client_contact_id": "",
"created_at": 0,
"email_error": "",
"email_status": "",
"id": "",
"key": "",
"link": "",
"message_id": "",
"opened_date": "",
"sent_date": "",
"updated_at": 0,
"viewed_date": ""
}
],
"is_amount_discount": false,
"is_deleted": false,
"last_sent_date": "",
"line_items": [
{
"cost": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"discount": 0,
"expense_id": "",
"gross_line_total": 0,
"income_account_id": "",
"is_amount_discount": false,
"line_total": 0,
"net_cost": 0,
"notes": "",
"product_cost": 0,
"product_key": "",
"quantity": 0,
"sort_id": "",
"tags": "",
"task_id": "",
"tax_amount": 0,
"tax_id": "",
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"type_id": "",
"unit_code": ""
}
],
"location_id": "",
"next_send_date": "",
"number": "",
"paid_to_date": 0,
"partial": 0,
"partial_due_date": "",
"po_number": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"recurring_id": "",
"reminder1_sent": "",
"reminder2_sent": "",
"reminder3_sent": "",
"reminder_last_sent": "",
"status_id": "",
"subscription_id": "",
"sync": null,
"tags": [],
"tax_info": {},
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"terms": "",
"total_taxes": 0,
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"data": [
{
"address1": "",
"address2": "",
"archived_at": 0,
"city": "",
"client_id": "",
"country_id": "",
"created_at": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"id": "",
"is_deleted": false,
"is_shipping_location": false,
"name": "",
"postal_code": "",
"state": "",
"tax_data": {},
"updated_at": 0,
"user_id": "",
"vendor_id": ""
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"data": [
{
"archived_at": 0,
"created_at": 0,
"id": "",
"is_deleted": false,
"name": "",
"num_days": 0,
"updated_at": 0
}
]
}
+52
View File
@@ -0,0 +1,52 @@
{
"data": [
{
"amount": 0,
"applied": 0,
"archived_at": 0,
"assigned_user_id": "",
"client_contact_id": "",
"client_id": "",
"company_gateway_id": "",
"created_at": 0,
"currency_id": "",
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"documents": [],
"exchange_currency_id": "",
"exchange_rate": 0,
"gateway_type_id": "",
"id": "",
"invitation_id": "",
"is_deleted": false,
"is_manual": false,
"number": "",
"paymentables": [
{
"amount": 0,
"archived_at": 0,
"created_at": 0,
"id": "",
"invoice_id": "",
"payment_id": "",
"refunded": 0,
"updated_at": 0
}
],
"private_notes": "",
"project_id": "",
"refunded": 0,
"status_id": "",
"tags": [],
"transaction_id": "",
"transaction_reference": "",
"type_id": "",
"updated_at": 0,
"user_id": "",
"vendor_id": ""
}
]
}
+37
View File
@@ -0,0 +1,37 @@
{
"data": [
{
"archived_at": 0,
"assigned_user_id": "",
"cost": 0,
"created_at": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"documents": [],
"id": "",
"in_stock_quantity": 0,
"income_account_id": "",
"is_deleted": false,
"max_quantity": 0,
"notes": "",
"price": 0,
"product_image": "",
"product_key": "",
"quantity": 0,
"stock_notification": false,
"stock_notification_threshold": 0,
"tags": [],
"tax_id": "",
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"updated_at": 0,
"user_id": ""
}
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"data": [
{
"archived_at": 0,
"assigned_user_id": "",
"budgeted_amount": 0,
"budgeted_hours": 0,
"client_id": "",
"color": "",
"created_at": 0,
"current_hours": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"documents": [],
"due_date": "",
"id": "",
"is_deleted": false,
"name": "",
"number": "",
"private_notes": "",
"public_notes": "",
"tags": [],
"task_rate": 0,
"updated_at": 0,
"user_id": ""
}
]
}
+93
View File
@@ -0,0 +1,93 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"balance": 0,
"client_id": "",
"created_at": 0,
"currency_id": "",
"custom_surcharge1": 0,
"custom_surcharge2": 0,
"custom_surcharge3": 0,
"custom_surcharge4": 0,
"custom_surcharge_tax1": false,
"custom_surcharge_tax2": false,
"custom_surcharge_tax3": false,
"custom_surcharge_tax4": false,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"design_id": "",
"discount": 0,
"documents": [],
"due_date": "",
"e_invoice": {},
"entity_type": "",
"exchange_rate": 0,
"expense_id": "",
"footer": "",
"has_expenses": false,
"has_tasks": false,
"id": "",
"invitations": [
{
"archived_at": 0,
"can_sign": false,
"created_at": 0,
"email_error": "",
"email_status": "",
"id": "",
"key": "",
"link": "",
"message_id": "",
"opened_date": "",
"sent_date": "",
"updated_at": 0,
"vendor_contact_id": "",
"viewed_date": ""
}
],
"invoice_id": "",
"is_amount_discount": false,
"is_deleted": false,
"last_sent_date": "",
"line_items": [],
"location_id": "",
"next_send_date": "",
"number": "",
"paid_to_date": 0,
"partial": 0,
"partial_due_date": "",
"po_number": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"quote_id": "",
"reminder1_sent": "",
"reminder2_sent": "",
"reminder3_sent": "",
"reminder_last_sent": "",
"status_id": "",
"subscription_id": "",
"sync": null,
"tags": [],
"tax_info": {},
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"terms": "",
"total_taxes": 0,
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+123
View File
@@ -0,0 +1,123 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"balance": 0,
"client_id": "",
"created_at": 0,
"custom_surcharge1": 0,
"custom_surcharge2": 0,
"custom_surcharge3": 0,
"custom_surcharge4": 0,
"custom_surcharge_tax1": false,
"custom_surcharge_tax2": false,
"custom_surcharge_tax3": false,
"custom_surcharge_tax4": false,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"design_id": "",
"discount": 0,
"documents": [],
"due_date": "",
"e_invoice": {},
"entity_type": "",
"exchange_rate": 0,
"footer": "",
"has_expenses": false,
"has_tasks": false,
"id": "",
"invitations": [
{
"archived_at": 0,
"can_sign": false,
"client_contact_id": "",
"created_at": 0,
"email_error": "",
"email_status": "",
"id": "",
"key": "",
"link": "",
"message_id": "",
"opened_date": "",
"sent_date": "",
"updated_at": 0,
"viewed_date": ""
}
],
"invoice_id": "",
"is_amount_discount": false,
"is_deleted": false,
"last_sent_date": "",
"line_items": [
{
"cost": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"discount": 0,
"expense_id": "",
"gross_line_total": 0,
"income_account_id": "",
"is_amount_discount": false,
"line_total": 0,
"net_cost": 0,
"notes": "",
"product_cost": 0,
"product_key": "",
"quantity": 0,
"sort_id": "",
"tags": "",
"task_id": "",
"tax_amount": 0,
"tax_id": "",
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"type_id": "",
"unit_code": ""
}
],
"location_id": "",
"next_send_date": "",
"number": "",
"paid_to_date": 0,
"partial": 0,
"partial_due_date": "",
"po_number": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"reminder1_sent": "",
"reminder2_sent": "",
"reminder3_sent": "",
"reminder_last_sent": "",
"status_id": "",
"subscription_id": "",
"sync": null,
"tags": [],
"tax_info": {},
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"terms": "",
"total_taxes": 0,
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+59
View File
@@ -0,0 +1,59 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"bank_id": "",
"calculate_tax_by_amount": false,
"category_id": "",
"client_id": "",
"created_at": 0,
"currency_id": "",
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"documents": [],
"entity_type": "",
"exchange_rate": 0,
"foreign_amount": 0,
"frequency_id": "",
"id": "",
"invoice_currency_id": "",
"invoice_documents": false,
"invoice_id": "",
"is_deleted": false,
"last_sent_date": "",
"next_send_date": "",
"number": "",
"payment_date": "",
"payment_type_id": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"recurring_dates": [],
"recurring_expense_currency_id": "",
"remaining_cycles": 0,
"should_be_invoiced": false,
"status_id": "",
"tags": [],
"tax_amount1": 0,
"tax_amount2": 0,
"tax_amount3": 0,
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"transaction_id": "",
"transaction_reference": "",
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+90
View File
@@ -0,0 +1,90 @@
{
"data": [
{
"amount": 0,
"archived_at": 0,
"assigned_user_id": "",
"auto_bill": "",
"auto_bill_enabled": false,
"balance": 0,
"client_id": "",
"created_at": 0,
"custom_surcharge1": 0,
"custom_surcharge2": 0,
"custom_surcharge3": 0,
"custom_surcharge4": 0,
"custom_surcharge_tax1": false,
"custom_surcharge_tax2": false,
"custom_surcharge_tax3": false,
"custom_surcharge_tax4": false,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"design_id": "",
"discount": 0,
"documents": [],
"due_date": "",
"due_date_days": "",
"e_invoice": {},
"entity_type": "",
"exchange_rate": 0,
"footer": "",
"frequency_id": "",
"has_expenses": false,
"has_tasks": false,
"id": "",
"invitations": [
{
"archived_at": 0,
"can_sign": false,
"client_contact_id": "",
"created_at": 0,
"email_error": "",
"email_status": "",
"id": "",
"key": "",
"link": "",
"message_id": "",
"opened_date": "",
"sent_date": "",
"updated_at": 0,
"viewed_date": ""
}
],
"is_amount_discount": false,
"is_deleted": false,
"last_sent_date": "",
"line_items": [],
"location_id": "",
"next_send_date": "",
"next_send_datetime": "",
"number": "",
"paid_to_date": 0,
"partial": 0,
"partial_due_date": "",
"po_number": "",
"private_notes": "",
"project_id": "",
"public_notes": "",
"recurring_dates": [],
"remaining_cycles": 0,
"status_id": "",
"subscription_id": "",
"tags": [],
"tax_name1": "",
"tax_name2": "",
"tax_name3": "",
"tax_rate1": 0,
"tax_rate2": 0,
"tax_rate3": 0,
"terms": "",
"total_taxes": 0,
"updated_at": 0,
"user_id": "",
"uses_inclusive_taxes": false,
"vendor_id": ""
}
]
}
+185
View File
@@ -0,0 +1,185 @@
{
"bulk_updates": {
"client": [
""
],
"expense": [
""
],
"recurring_invoice": [
""
],
"task": [
""
]
},
"countries": [
{
"capital": "",
"citizenship": "",
"country_code": "",
"currency": "",
"currency_code": "",
"currency_sub_unit": "",
"decimal_separator": "",
"eea": false,
"full_name": "",
"id": "",
"iso_3166_2": "",
"iso_3166_3": "",
"name": "",
"region_code": "",
"sub_region_code": "",
"swap_currency_symbol": false,
"swap_postal_code": false,
"thousand_separator": ""
}
],
"currencies": [
{
"code": "",
"decimal_separator": "",
"exchange_rate": 0,
"id": "",
"name": "",
"precision": 0,
"swap_currency_symbol": false,
"symbol": "",
"thousand_separator": ""
}
],
"date_formats": [
{
"format": "",
"format_dart": "",
"format_moment": "",
"id": ""
}
],
"datetime_formats": [
{
"format": "",
"format_dart": "",
"format_moment": "",
"id": ""
}
],
"gateways": [
{
"created_at": 0,
"default_gateway_type_id": "",
"fields": "",
"id": "",
"is_offsite": false,
"is_secure": false,
"key": "",
"name": "",
"options": {
"1": {
"refund": false,
"token_billing": false,
"webhooks": [
""
]
},
"2": {
"refund": false,
"token_billing": false,
"webhooks": [
""
]
}
},
"provider": "",
"site_url": "",
"sort_order": 0,
"updated_at": 0,
"visible": false
}
],
"industries": [
{
"id": "",
"name": ""
}
],
"languages": [
{
"id": "",
"locale": "",
"name": ""
}
],
"license_key": false,
"payment_types": [
{
"gateway_type_id": 0,
"id": "",
"name": ""
}
],
"sizes": [
{
"id": "",
"name": ""
}
],
"templates": {
"invoice": {
"body": "",
"subject": ""
},
"partial_payment": {
"body": "",
"subject": ""
},
"payment": {
"body": "",
"subject": ""
},
"payment_failed": {
"body": "",
"subject": ""
},
"purchase_order": {
"body": "",
"subject": ""
},
"quote": {
"body": "",
"subject": ""
},
"quote_reminder1": {
"body": "",
"subject": ""
},
"reminder1": {
"body": "",
"subject": ""
},
"reminder2": {
"body": "",
"subject": ""
},
"reminder3": {
"body": "",
"subject": ""
},
"reminder_endless": {
"body": "",
"subject": ""
},
"statement": {
"body": "",
"subject": ""
}
},
"timezones": [
{
"id": "",
"location": "",
"name": "",
"utc_offset": 0
}
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"App\\Livewire\\BillingPortal\\Authentication\\Login": {
"dependencies": [],
"id": ""
},
"App\\Livewire\\BillingPortal\\Authentication\\Register": {
"dependencies": [],
"id": ""
},
"App\\Livewire\\BillingPortal\\Authentication\\RegisterOrLogin": {
"dependencies": [],
"id": ""
},
"App\\Livewire\\BillingPortal\\Cart\\Cart": {
"dependencies": [],
"id": ""
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"data": [
{
"category_id": 0,
"client_id": "",
"company_id": "",
"created_at": 0,
"event_id": 0,
"id": "",
"log": "",
"type_id": 0,
"updated_at": 0,
"user_id": ""
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"data": [
{
"archived_at": 0,
"color": null,
"created_at": 0,
"entity_type": "",
"id": "",
"is_deleted": false,
"name": "",
"updated_at": 0
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"data": [
{
"archived_at": 0,
"color": "",
"created_at": 0,
"id": "",
"is_deleted": false,
"name": "",
"status_order": 0,
"updated_at": 0
}
]
}
+36
View File
@@ -0,0 +1,36 @@
{
"data": [
{
"archived_at": 0,
"assigned_user_id": "",
"client_id": "",
"created_at": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"date": "",
"description": "",
"documents": [],
"due_date": "",
"duration": 0,
"estimated_duration": null,
"id": "",
"invoice_id": "",
"is_date_based": false,
"is_deleted": false,
"is_running": false,
"meta": {},
"number": "",
"project_id": "",
"rate": 0,
"status_id": "",
"status_order": null,
"status_sort_order": 0,
"tags": [],
"time_log": "",
"updated_at": 0,
"user_id": ""
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"data": [
{
"archived_at": 0,
"created_at": 0,
"id": "",
"is_deleted": false,
"name": "",
"rate": 0,
"updated_at": 0
}
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"data": [
{
"archived_at": 0,
"created_at": 0,
"id": "",
"is_deleted": false,
"is_system": false,
"name": "",
"token": "",
"updated_at": 0,
"user_id": ""
}
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"data": [
{
"archived_at": 0,
"created_at": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"email": "",
"email_verified_at": 0,
"first_name": "",
"google_2fa_secret": false,
"has_password": false,
"id": "",
"is_deleted": false,
"language_id": "",
"last_confirmed_email_address": "",
"last_login": 0,
"last_name": "",
"oauth_provider_id": "",
"oauth_user_token": "",
"passkey_count": 0,
"passkey_enabled": false,
"phone": "",
"referral_code": "",
"referral_meta": {
"enterprise": 0,
"free": 0,
"pro": 0
},
"settings": {
"calendar_connection": {
"email": "",
"status": ""
}
},
"signature": "",
"updated_at": 0,
"user_logged_in_notification": false,
"verified_phone_number": false
}
]
}
+63
View File
@@ -0,0 +1,63 @@
{
"data": [
{
"address1": "",
"address2": "",
"archived_at": 0,
"assigned_user_id": "",
"city": "",
"classification": "",
"contacts": [
{
"archived_at": 0,
"can_sign": false,
"cc_only": false,
"created_at": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"email": "",
"first_name": "",
"id": "",
"is_primary": false,
"last_login": 0,
"last_name": "",
"link": "",
"password": "",
"phone": "",
"send_email": false,
"updated_at": 0
}
],
"country_id": "",
"created_at": 0,
"currency_id": "",
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"display_name": "",
"documents": [],
"id": "",
"id_number": "",
"is_deleted": false,
"is_tax_exempt": false,
"language_id": "",
"last_login": 0,
"name": "",
"number": "",
"phone": "",
"postal_code": "",
"private_notes": "",
"public_notes": "",
"routing_id": "",
"state": "",
"tags": [],
"updated_at": 0,
"user_id": "",
"vat_number": "",
"website": ""
}
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"data": [
{
"archived_at": 0,
"company_id": "",
"created_at": 0,
"event_id": "",
"format": "",
"headers": [],
"id": "",
"is_deleted": false,
"rest_method": "",
"target_url": "",
"updated_at": 0,
"user_id": ""
}
]
}
+227
View File
@@ -0,0 +1,227 @@
//! Decodes fixtures captured from a real server.
//!
//! `tests/fixtures/*.json` are written by `examples/capture_shapes.rs` and
//! hold only key names and JSON types (strings are `""`, numbers `0`, bools
//! `false`). Mock responses written from the upstream source have been wrong
//! about the real thing before, so these pin the shapes a server actually
//! sent. To refresh them, seed a disposable instance with
//! `examples/seed_sample_data.rs` and re-run the capture.
//!
//! Three properties are checked: every fixture decodes into its model
//! (goal 2), a decode followed by a re-serialise loses no key (goal 1), and no
//! fixture contains a real value, so a careless capture cannot leak data.
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use invoiceninjars::models::*;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
fn dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn load(name: &str) -> Value {
let path = dir().join(format!("{name}.json"));
let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
serde_json::from_str(&text).unwrap_or_else(|e| panic!("{}: {e}", path.display()))
}
/// The captured row of a list fixture.
fn row(name: &str) -> Value {
load(name)["data"][0].clone()
}
/// Key paths present in `raw` but absent from `out`. Arrays are compared
/// through their first element, which is all a fixture keeps.
fn lost_keys(raw: &Value, out: &Value, prefix: &str, into: &mut Vec<String>) {
match (raw, out) {
(Value::Object(raw), Value::Object(out)) => {
for (key, raw_value) in raw {
match out.get(key) {
Some(out_value) => {
lost_keys(raw_value, out_value, &format!("{prefix}.{key}"), into)
}
None => into.push(format!("{prefix}.{key}")),
}
}
}
(Value::Array(raw), Value::Array(out)) => {
if let (Some(raw), Some(out)) = (raw.first(), out.first()) {
lost_keys(raw, out, &format!("{prefix}[]"), into);
}
}
_ => {}
}
}
/// Decodes `raw` as `T`, re-serialises it, and returns problems.
fn check<T: DeserializeOwned + Serialize>(name: &str, raw: &Value) -> Vec<String> {
let typed: T = match serde_json::from_value(raw.clone()) {
Ok(typed) => typed,
Err(e) => return vec![format!("{name}: does not decode: {e}")],
};
let out = serde_json::to_value(&typed).expect("serialises");
let mut lost = Vec::new();
lost_keys(raw, &out, name, &mut lost);
lost.into_iter()
.filter(|path| !OMITTED_WHEN_EMPTY.contains(&path.as_str()))
.map(|path| format!("{path}: key lost on a decode/re-serialise"))
.collect()
}
/// Fields that are deliberately left out of a request when empty, because the
/// server rejects the empty value (`""` becomes `NULL` on a non-null column,
/// or fails an `integer` rule). A fixture holds `""` placeholders, so these
/// show up as absent after a re-serialise. Adding to this list is a decision.
const OMITTED_WHEN_EMPTY: &[&str] = &[
"locations.country_id",
"purchase_orders.status_id",
"webhooks.format",
];
/// One fixture per `(file, type)`; the macro also records which files it
/// covered so a new capture cannot sit untested.
macro_rules! list_fixtures {
($($file:literal => $ty:ty),+ $(,)?) => {
const COVERED_LISTS: &[&str] = &[$($file),+];
fn check_lists() -> Vec<String> {
let mut problems = Vec::new();
$( problems.extend(check::<$ty>($file, &row($file))); )+
problems
}
};
}
list_fixtures! {
"activities" => Activity,
"clients" => Client,
"companies" => Company,
"credits" => Credit,
"designs" => Design,
"documents" => Document,
"expense_categories" => ExpenseCategory,
"expenses" => Expense,
"group_settings" => GroupSetting,
"invoices" => Invoice,
"locations" => Location,
"payment_terms" => PaymentTerm,
"payments" => Payment,
"products" => Product,
"projects" => Project,
"purchase_orders" => PurchaseOrder,
"quotes" => Quote,
"recurring_expenses" => RecurringExpense,
"recurring_invoices" => RecurringInvoice,
"system_logs" => SystemLog,
"tags" => Tag,
"task_statuses" => TaskStatus,
"tasks" => Task,
"tax_rates" => TaxRate,
"tokens" => CompanyToken,
"users" => User,
"vendors" => Vendor,
"webhooks" => Webhook,
}
/// Fixtures that are not a one-row list.
const COVERED_OTHER: &[&str] = &[
"statics",
"health_check",
"subscription_steps",
"activity_feed",
];
#[test]
fn every_real_row_decodes_and_loses_no_key() {
let problems = check_lists();
assert!(
problems.is_empty(),
"{} problem(s):\n{}",
problems.len(),
problems.join("\n")
);
}
#[test]
fn bare_endpoint_fixtures_decode() {
let mut problems = Vec::new();
problems.extend(check::<Statics>("statics", &load("statics")));
problems.extend(check::<HealthCheck>("health_check", &load("health_check")));
problems.extend(check::<ActivityFeedEntry>(
"activity_feed",
&load("activity_feed")["data"][0],
));
// Keyed by class name on a real server; see `subscription_steps`.
let steps: Result<BTreeMap<String, SubscriptionStep>, _> =
serde_json::from_value(load("subscription_steps"));
match steps {
Ok(steps) => assert!(!steps.is_empty(), "the captured steps object is empty"),
Err(e) => problems.push(format!("subscription_steps: {e}")),
}
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
#[test]
fn every_fixture_file_is_covered_by_a_test() {
let mut on_disk: Vec<String> = fs::read_dir(dir())
.expect("tests/fixtures exists")
.filter_map(|entry| {
let path = entry.ok()?.path();
(path.extension()? == "json").then(|| path.file_stem()?.to_str().map(String::from))?
})
.collect();
on_disk.sort();
let mut covered: Vec<String> = COVERED_LISTS
.iter()
.chain(COVERED_OTHER)
.map(|s| s.to_string())
.collect();
covered.sort();
assert_eq!(
on_disk, covered,
"a captured fixture has no test (or a test has no fixture)"
);
}
/// The capture tool replaces every value; this refuses to pass if any
/// survived, so real data cannot be committed by accident.
#[test]
fn fixtures_hold_shapes_and_never_values() {
fn walk(value: &Value, at: &str, bad: &mut Vec<String>) {
match value {
Value::String(s) if !s.is_empty() => bad.push(format!("{at}: string value")),
Value::Number(n) if n.as_f64() != Some(0.0) => bad.push(format!("{at}: number value")),
Value::Bool(true) => bad.push(format!("{at}: true")),
Value::Array(items) => {
assert!(items.len() <= 1, "{at}: a fixture keeps one array element");
items.iter().for_each(|v| walk(v, &format!("{at}[]"), bad));
}
Value::Object(map) => map
.iter()
.for_each(|(k, v)| walk(v, &format!("{at}.{k}"), bad)),
_ => {}
}
}
let mut bad = Vec::new();
for entry in fs::read_dir(dir()).expect("tests/fixtures exists") {
let path = entry.unwrap().path();
if path.extension().is_some_and(|e| e == "json") {
let name = path.file_stem().unwrap().to_string_lossy().to_string();
walk(&load(&name), &name, &mut bad);
}
}
assert!(
bad.is_empty(),
"fixtures contain values:\n{}",
bad.join("\n")
);
}