Wrap the eight invitation-key downloads
`GOALS.md` listed these as "eight routes that take no auth token, so they do
not fit the authenticated request path and need a different entry point."
That was wrong on every count, and wrong in the way goal 6 exists to catch:
the premise came from the route names rather than the route file.
All eight sit inside `routes/api.php`'s `api/v1` group, under its
`token_auth` middleware, and the controllers' own OpenAPI blocks declare
`X-API-TOKEN`. So they needed no new entry point — just a macro, alongside
`entity_action_methods!` and for the same four entities. What the item had
actually described is the client-portal family in `routes/client.php`, which
is genuinely token-less, is not under `/api/v1`, and stays unwrapped for
that reason; it is now the gap entry in `COVERAGE.md`.
`download_{invoice,quote,credit,purchase_order}_by_invitation_key` returns
the PDF and `download_e_*_by_invitation_key` the XML e-document. These take
a contact's invitation key rather than a record id, so the server renders in
that contact's locale — a client with contacts in two languages gets a
different document per key, which the id-based `download_*` cannot express.
Two things here are easy to get wrong, so both are pinned by tests. The
entity segment is singular (`invoice/{key}/download`), the only route family
in the API that departs from the plural CRUD path. And an unknown key is a
400 carrying `no record found`: the controllers return that directly instead
of raising `ModelNotFoundException`, so the status matches the rule for a
missing record but the message does not.
The smoke test's read tier now downloads an invoice by invitation key,
taking the key from the `invitations` array the list route already returns.
It is there to re-check the token claim against a real server rather than
against a second reading of the source. The `download_e_*` four are left
out: they render from e-invoice settings a test instance need not have.
Also in here, unrelated: a rustfmt fix to a `match` arm in
`examples/smoke_test.rs` that was already off-format on main, so
`cargo fmt --check` — and with it CI's `test` job — was failing before this
change. It could not be split out cleanly, being in a file this change also
touches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+42
-5
@@ -332,6 +332,42 @@ async fn read_checks(client: &InvoiceNinjaClient, raw: &Raw, report: &mut Report
|
||||
Err(e) => report.fail("live_preview", describe(&e)),
|
||||
}
|
||||
|
||||
// The invitation-key download, on the singular `invoice/{key}/download`
|
||||
// route. Worth a real-server check for one specific reason: this was
|
||||
// listed as unwrappable for months on the belief that it takes no auth
|
||||
// token, and the route file says otherwise. If that reading is still
|
||||
// wrong, the token we send is either ignored or rejected, and this is
|
||||
// where it shows. The key comes from the invoice's own `invitations`
|
||||
// array, which the list route returns without an `include`.
|
||||
//
|
||||
// Only the PDF, not the e-document: `download_e_invoice` renders from
|
||||
// the company's e-invoice settings, so an instance with none configured
|
||||
// would fail here for a reason that is not the client's fault.
|
||||
match client.list_invoices(&small).await {
|
||||
Ok(rows) => match rows.iter().find_map(|i| {
|
||||
i.extra
|
||||
.get("invitations")?
|
||||
.as_array()?
|
||||
.first()?
|
||||
.get("key")?
|
||||
.as_str()
|
||||
}) {
|
||||
Some(key) => match client.download_invoice_by_invitation_key(key).await {
|
||||
Ok(pdf) => report.check(
|
||||
"download_invoice_by_invitation_key",
|
||||
pdf.starts_with(b"%PDF"),
|
||||
format!("{} byte(s), starts with the PDF magic", pdf.len()),
|
||||
),
|
||||
Err(e) => report.fail("download_invoice_by_invitation_key", describe(&e)),
|
||||
},
|
||||
None => report.pass(
|
||||
"download_invoice_by_invitation_key",
|
||||
"skipped: no invoice with an invitation on this instance",
|
||||
),
|
||||
},
|
||||
Err(e) => report.fail("download_invoice_by_invitation_key", describe(&e)),
|
||||
}
|
||||
|
||||
list!(report, "list_clients", client.list_clients(&small));
|
||||
list!(report, "list_invoices", client.list_invoices(&small));
|
||||
list!(report, "list_quotes", client.list_quotes(&small));
|
||||
@@ -817,11 +853,12 @@ async fn password_checks(client: &InvoiceNinjaClient, report: &mut Report, passw
|
||||
// ModelNotFoundException to a 400 and keeps 404 for a missing route. The
|
||||
// message is checked too, so a 400 raised for some other reason fails.
|
||||
match client.get_client(&keep).await {
|
||||
Err(Error::Api { status, message, .. })
|
||||
if status.as_u16() == 400 && message.contains("No query results") =>
|
||||
{
|
||||
report.pass("get_client after purge", "400 no query results, the row is gone")
|
||||
}
|
||||
Err(Error::Api {
|
||||
status, message, ..
|
||||
}) if status.as_u16() == 400 && message.contains("No query results") => report.pass(
|
||||
"get_client after purge",
|
||||
"400 no query results, the row is gone",
|
||||
),
|
||||
Err(e) => report.fail("get_client after purge", describe(&e)),
|
||||
Ok(_) => report.fail("get_client after purge", "still readable after a purge"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user