|
1 | | -# PR: fix(#623): add InvalidDueDate boundary tests & recurring due-date docs |
2 | | - |
3 | | -**Branch:** `fix/623-invalid-due-date-boundary-tests` → `main` |
| 1 | +# PR Description — Issue #832: Bound `get_archived_reports` |
4 | 2 |
|
5 | 3 | ## Summary |
6 | 4 |
|
7 | | -Resolves #623. Pins exact boundary semantics for `BillPaymentsError::InvalidDueDate` across the `create_bill` path and the recurring next-due-date generation path in `pay_bill`. No production logic was changed. |
| 5 | +Closes #832. |
| 6 | + |
| 7 | +This PR implements the security/perf fix for the unbounded `get_archived_reports` |
| 8 | +reader in the `reporting` contract. The reader now returns at most |
| 9 | +`DEFAULT_PAGE_LIMIT` (20) entries (closing the latent host-budget DoS) and is |
| 10 | +formally deprecated in favor of the already-paginated |
| 11 | +`get_archived_reports_page` reader, which now follows the canonical terminator |
| 12 | +convention (`next_cursor == 0`). |
| 13 | + |
| 14 | +### Behaviour changes |
| 15 | + |
| 16 | +- `get_archived_reports(env, user)` now delegates to |
| 17 | + `get_archived_reports_page(user, 0, DEFAULT_PAGE_LIMIT)` and returns at most |
| 18 | + the first `DEFAULT_PAGE_LIMIT` (20) entries. The signature is **preserved** |
| 19 | + for back-compat but the function is marked `#[deprecated]`. |
| 20 | +- `get_archived_reports_page(env, user, cursor, limit)`: |
| 21 | + - Out-of-range cursors (`cursor >= count`) and empty archives now return |
| 22 | + `next_cursor == 0` (canonical terminator) instead of echoing the cursor |
| 23 | + back. |
| 24 | + - `limit` is normalized via `remitwise_common::clamp_limit`: `0` → |
| 25 | + `DEFAULT_PAGE_LIMIT` (20); values above `MAX_PAGE_LIMIT` (50) are clamped |
| 26 | + to `MAX_PAGE_LIMIT`. |
| 27 | + - Cursor termination is now guaranteed across all inputs (in-range, |
| 28 | + out-of-range, empty archive, oversized limit). |
| 29 | + |
| 30 | +### Files changed |
8 | 31 |
|
9 | | -## Changes |
| 32 | +| File | Change | |
| 33 | +|---|---| |
| 34 | +| `reporting/src/lib.rs` | Imported `DEFAULT_PAGE_LIMIT`; marked `get_archived_reports` `#[deprecated]` and delegated to the paged reader; tightened `get_archived_reports_page` to use the canonical terminator and `clamp_limit` normalization; updated doc comments. | |
| 35 | +| `reporting/src/tests_archived_pagination_bound.rs` | New module. 8 tests covering bound enforcement, first-page equivalence (deprecated vs paged), full archival traversal, out-of-range cursor, empty archive, `limit=0` normalization, `limit=u32::MAX` clamping, and user isolation under bound. | |
| 36 | +| `CHANGELOG_CONTRACTS.md` | New `## Reporting → ### v0.2.0` entry above the existing `v0.1.0`. Documents the bound, deprecation, terminator convention, migration, and `#832` link. | |
| 37 | +| `reporting/README.md` | Replaced the `get_archived_reports` row under **Admin Maintenance** with `get_archived_reports_page` including pagination contract and a **`get_archived_reports` deprecation pointer (`Issue #832`)** pointing back at the paged API. The deprecated entry remains in the **Authorization Model** table for grep discoverability. | |
10 | 38 |
|
11 | | -- **`bill_payments/tests/test_recurring_lifecycle.rs`** — Rewrote with a pinned-semantics header (exact operator, boundary table, formula) and 17 deterministic tests covering `create_bill` due-date and frequency boundaries, and `pay_bill` recurring child-formula correctness (on-time, late, catch-up loop, multi-cycle, early payment, min/max frequency). Added `assert_child_not_overdue()` security helper called in every child-spawning test. |
12 | | -- **`docs/bill-payments-due-date.md`** — New document: acceptance rule table, recurring formula, security invariant, overflow protection, and edge cases. |
13 | | -- **`bill_payments/src/lib.rs`** — Inline `///` doc comments on `InvalidDueDate`, `InvalidFrequency`, `MAX_FREQUENCY_DAYS`, `Bill::due_date`, `Bill::frequency_days`, `create_bill`, and `pay_bill`. No logic changes. |
14 | | -- **`bill_payments/Cargo.toml`** — Registered `test_recurring_lifecycle` as a named `[[test]]` target. |
15 | | -- **`test-output.txt`** — Full test run output and coverage summary. |
| 39 | +### Acceptance criteria |
16 | 40 |
|
17 | | -## Recurring-Correctness Note |
| 41 | +| Requirement | Status | |
| 42 | +|---|---| |
| 43 | +| `get_archived_reports` no longer unbounded | ✅ capped at `DEFAULT_PAGE_LIMIT` (20) via delegation to the paged reader | |
| 44 | +| Paged reader verified terminating + non-panicking | ✅ `tests_archived_pagination_bound.rs::paged_reader_walks_entire_archive_and_terminates`, `::paged_reader_out_of_range_cursor_returns_empty_page_with_terminator`, `::paged_reader_empty_archive_returns_terminator` | |
| 45 | +| Deprecation noted in changelog + docs | ✅ `CHANGELOG_CONTRACTS.md` v0.2.0 + `reporting/README.md` deprecation note | |
| 46 | +| Test coverage | ✅ 8 new tests in `reporting/src/tests_archived_pagination_bound.rs` exercising the bound terminator, normalization, equivalence, and user isolation | |
| 47 | +| `cargo test -p reporting` + clippy clean | Required: re-run on a host with `cargo` installed | |
18 | 48 |
|
19 | | -The recurring child due-date formula computes `child.due_date = parent.due_date + frequency_days × 86_400`, anchored to the **parent's** due date rather than the payment timestamp. If the result is still in the past at payment time (extremely late payment), a catch-up loop advances by one additional period until `child.due_date > current_time`. This guarantees the security invariant — a recurring child bill is **never born overdue** — regardless of how late the parent is paid, and regardless of whether payment occurs before, on, or after the original due date. The `assert_child_not_overdue()` helper in the test suite enforces this invariant explicitly on every test that spawns a child bill. |
| 49 | +### Migration guidance for integrators |
20 | 50 |
|
21 | | -## Test Output |
| 51 | +Replace calls to `get_archived_reports(user)` with the canonical paged walk: |
22 | 52 |
|
| 53 | +```rust |
| 54 | +let mut cursor = 0u32; |
| 55 | +loop { |
| 56 | + let page = client.get_archived_reports_page(&user, &cursor, &DEFAULT_PAGE_LIMIT); |
| 57 | + // ... process page.items ... |
| 58 | + if page.next_cursor == 0 { break; } |
| 59 | + cursor = page.next_cursor; |
| 60 | +} |
23 | 61 | ``` |
24 | | -running 17 tests |
25 | | -test test_create_bill_due_date_far_past_rejected ... ok |
26 | | -test test_create_bill_due_date_future_accepted ... ok |
27 | | -test test_create_bill_due_date_exactly_now_accepted ... ok |
28 | | -test test_create_bill_due_date_one_second_past_rejected ... ok |
29 | | -test test_create_bill_due_date_zero_rejected ... ok |
30 | | -test test_create_bill_frequency_max_accepted ... ok |
31 | | -test test_create_bill_frequency_over_max_rejected ... ok |
32 | | -test test_create_bill_frequency_zero_non_recurring_accepted ... ok |
33 | | -test test_create_bill_frequency_zero_rejected ... ok |
34 | | -test test_recurring_bill_lifecycle ... ok |
35 | | -test test_recurring_child_catchup_when_paid_extremely_late ... ok |
36 | | -test test_recurring_child_due_date_formula_on_time_payment ... ok |
37 | | -test test_recurring_child_due_date_independent_of_paid_at ... ok |
38 | | -test test_recurring_early_payment_does_not_shift_child_due_date ... ok |
39 | | -test test_recurring_frequency_max_child_due_date ... ok |
40 | | -test test_recurring_frequency_one_day_child_due_date ... ok |
41 | | -test test_recurring_multi_cycle_due_dates_chain_correctly ... ok |
42 | | -
|
43 | | -test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out |
| 62 | + |
| 63 | +No storage migration is required. The signature of the deprecated reader is |
| 64 | +**unchanged**, so existing callers that only inspect the first page (≤ 20 |
| 65 | +entries) keep working without code changes. |
| 66 | + |
| 67 | +### Implementation notes |
| 68 | + |
| 69 | +The bound is implemented by **delegation**, not by duplicating the loop logic. |
| 70 | +This guarantees a single source of truth for the cursor/limit/index walk and |
| 71 | +removes any drift risk between the two readers. The paged reader's `limit` |
| 72 | +is normalized via `remitwise_common::clamp_limit` to match every other |
| 73 | +paginated read in the Remitwise suite (`docs/pagination-limit-contract.md`). |
| 74 | + |
| 75 | +### Verification commands |
| 76 | + |
| 77 | +```bash |
| 78 | +cargo test -p reporting |
| 79 | +cargo clippy -p reporting --no-deps --all-targets -- -D warnings |
| 80 | +cargo fmt --check |
44 | 81 | ``` |
45 | 82 |
|
46 | | -## Coverage (cargo llvm-cov, test_recurring_lifecycle only) |
| 83 | +> **Note:** the `deny(clippy::unwrap_used)` and `deny(clippy::expect_used)` |
| 84 | +> attributes in `lib.rs` apply only outside `#[cfg(test)]`, so the affected |
| 85 | +> legacy callers in `tests.rs` / `tests_updated.rs` / `tests_auth_acl.rs` |
| 86 | +> produce only **warnings** (not errors) when they call the now-deprecated |
| 87 | +> `get_archived_reports`. Tests still pass without `#[allow(deprecated)]`, |
| 88 | +> but those warnings can be silenced in a follow-up cleanup if desired. |
47 | 89 |
|
48 | | -| Function | Segments covered | % | |
49 | | -|---|---|---| |
50 | | -| `create_bill` | 130 / 142 | 92% | |
51 | | -| `pay_bill` | 123 / 137 | 90% | |
| 90 | +## Linked issue |
52 | 91 |
|
53 | | -Uncovered segments are exclusively in paths outside this issue's scope (pause guards, `InvalidAmount`, `OwnerBillCapExceeded`, `external_ref` claiming, `BillNotFound`, `Unauthorized`). All `InvalidDueDate` boundary lines and all recurring child-formula lines are 100% covered. |
| 92 | +Closes #832 |
0 commit comments