mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
SaaS fixes (#6578)
Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: James Brunton <[email protected]> Co-authored-by: Reece Browne <[email protected]> Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Reece <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: Ludy <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
James Brunton
Reece Browne
ConnorYoh
Reece
EthanHealy01
Ludy
parent
96accea984
commit
ddf78d11ae
@@ -1,58 +0,0 @@
|
||||
# PAYG cucumber scenarios
|
||||
|
||||
End-to-end coverage for the PAYG shadow charging engine (PR #6519 / PR-S3
|
||||
in `notes/PAYG_DESIGN.md` §7.5).
|
||||
|
||||
## Running locally
|
||||
|
||||
```bash
|
||||
./testing/test-payg.sh
|
||||
```
|
||||
|
||||
That script:
|
||||
1. Boots `testing/compose/docker-compose-saas.yml` (Stirling-PDF with
|
||||
`STIRLING_FLAVOR=saas` + a Postgres holding the `stirling_pdf` schema)
|
||||
2. Waits for backend health
|
||||
3. Pipes `testing/compose/payg/saas-seed.sql` into the test postgres,
|
||||
creating a `payg-cucumber-team` flipped to `wallet_policy.engine =
|
||||
'PAYG_SHADOW'` and a test user with API key `payg-cucumber-key`
|
||||
4. Runs `python -m behave features/payg`
|
||||
5. Tears the stack down
|
||||
|
||||
## What's covered (automated, run by `docker-compose-tests-saas.yml` on CI)
|
||||
|
||||
| Scenario | Validates |
|
||||
|---|---|
|
||||
| First tool call writes a CHARGED row | Filter + interceptor fire end-to-end; shadow row written |
|
||||
| Lineage join — second call on output | `JobService.joinOrOpen` lineage matching; no new shadow row |
|
||||
| First-step 5xx refunds + closes the process | `markFirstStepFailed` flips row to REFUNDED, job to CLOSED |
|
||||
| 4xx leaves the row CHARGED | "customer paid for the attempt" semantics |
|
||||
| ZIP-returning tool records per-PDF OUTPUT | `PaygOutputExtractor` unpacks + records signatures |
|
||||
| Multi-file input writes a single shadow row | Multi-input group sizing |
|
||||
| `X-Stirling-Automation` sets PIPELINE source | Header → `JobSource` detection |
|
||||
|
||||
The 5xx scenario drives the refund path through `PaygCucumberThrowController`
|
||||
— a `@Profile("payg-cucumber")` stub in `app/saas/.../payg/test/` that always
|
||||
throws. The profile is activated by `docker-compose-saas.yml`'s
|
||||
`SPRING_PROFILES_ACTIVE=saas,payg-cucumber` and is never set in production.
|
||||
|
||||
## Manual-only scenarios
|
||||
|
||||
One part of the shadow engine can't reasonably be driven from this suite
|
||||
and is verified by hand each time its code path changes. The procedure
|
||||
lives in `notes/PAYG_DESIGN.md` §7.5.2 "PAYG cucumber: manual-only
|
||||
scenarios".
|
||||
|
||||
- **Kill-switch (`PAYG_FILTER_ENABLED=false`).** Needs a docker-container
|
||||
restart mid-suite; orchestrating that in behave is more harness fragility
|
||||
than it's worth for a flag that's only flipped during incident response.
|
||||
|
||||
## Fixtures
|
||||
|
||||
The scenarios reuse `testing/cucumber/exampleFiles/`:
|
||||
- `ghost1.pdf` — single-page reference PDF
|
||||
- `tables.pdf` — multi-page input for split / ZIP scenarios
|
||||
|
||||
If those filenames change in the main cucumber harness, update
|
||||
`features/steps/payg_step_definitions.py` SINGLE_PAGE_PDF / THREE_PAGE_PDF
|
||||
constants to match.
|
||||
@@ -1,105 +0,0 @@
|
||||
Feature: PAYG shadow-mode charging
|
||||
# End-to-end coverage for the PAYG shadow charging engine via the filter +
|
||||
# interceptor stack landed in PR #6519. Runs against the saas-profile
|
||||
# docker-compose target (testing/compose/docker-compose-saas.yml).
|
||||
#
|
||||
# Each scenario sets up a test team in PAYG_SHADOW mode, hits a real tool
|
||||
# endpoint, then asserts the shape of the rows written to payg_shadow_charge
|
||||
# and processing_job / processing_job_step.
|
||||
#
|
||||
# Lives under features/payg so it's only loaded when the saas-cucumber job
|
||||
# explicitly includes this directory (separate from the main behave run
|
||||
# which boots the proprietary-flavor stack and has no PAYG tables).
|
||||
|
||||
Background:
|
||||
Given the SaaS stack is running with PAYG enabled
|
||||
And team "payg-cucumber-team" exists with wallet_policy.engine = "PAYG_SHADOW"
|
||||
And I am authenticated as a member of team "payg-cucumber-team"
|
||||
|
||||
Scenario: First tool call writes a CHARGED shadow row
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST a single-page PDF to "/api/v1/security/add-password"
|
||||
Then the response status is 200
|
||||
And exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
And the latest shadow charge row has status "CHARGED"
|
||||
And the latest shadow charge row has payg_units >= 1
|
||||
And the latest shadow charge row's job is OPEN
|
||||
And the latest job has 1 step recorded with status "OK"
|
||||
|
||||
Scenario: Lineage join — second call on the same output joins the first process
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST a single-page PDF to "/api/v1/security/add-password"
|
||||
And I take the response body as "step1-output"
|
||||
And I POST "step1-output" to "/api/v1/security/sanitize-pdf"
|
||||
Then exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
# The second call joined the first process — no new shadow row
|
||||
And the latest job has step_count = 2
|
||||
And the latest job is OPEN
|
||||
|
||||
Scenario: First-step 5xx refunds the shadow row + closes the process
|
||||
# No reliably-5xx-ing real tool endpoint exists (every malformed input is
|
||||
# caught as 4xx by GlobalExceptionHandler), so we drive the refund path
|
||||
# through PaygCucumberThrowController — a @Profile("payg-cucumber") stub
|
||||
# that always throws IllegalStateException → 500. The profile is active
|
||||
# only inside docker-compose-saas.yml, never in production.
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST a single-page PDF to "/api/v1/payg-cucumber/throw-500"
|
||||
Then the response status is 500
|
||||
And exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
And the latest shadow charge row has status "REFUNDED"
|
||||
And the latest shadow charge row's refunded_at is not null
|
||||
And the latest shadow charge row's refund_reason starts with "first-step-5xx"
|
||||
And the latest job is CLOSED
|
||||
And the latest job has 1 step recorded with status "FAILED"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# NOTE: The kill-switch scenario (PAYG_FILTER_ENABLED=false) is NOT
|
||||
# automated — it requires restarting the stirling-pdf-saas container with
|
||||
# a different env var mid-run, which couples test setup to compose state
|
||||
# more than it's worth for a flag that only flips during incident response.
|
||||
# See notes/PAYG_DESIGN.md §7.5.2 "PAYG cucumber: manual-only scenarios".
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: 4xx leaves the shadow row CHARGED (customer pays for the attempt)
|
||||
# /sanitize-pdf on an encrypted PDF without the password reliably 400s
|
||||
# via GlobalExceptionHandler's PdfPasswordException → ProblemDetail path.
|
||||
# We chain: first call encrypts a PDF (CHARGED), second call tries to
|
||||
# sanitize WITHOUT the password and 400s. The 4xx assertion is on the
|
||||
# SECOND call's behaviour.
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST a single-page PDF to "/api/v1/security/add-password"
|
||||
And I take the response body as "encrypted"
|
||||
And I POST "encrypted" to "/api/v1/security/sanitize-pdf"
|
||||
Then the response status is >= 400 and < 500
|
||||
# Note: shadow_charges count is 1 because the second call lineage-joins
|
||||
# the first (its input matches the first's output). The 4xx therefore
|
||||
# appears as a FAILED step on the existing process, not a new shadow row.
|
||||
And exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
And the latest shadow charge row has status "CHARGED"
|
||||
And the latest job has step_count = 2
|
||||
And the latest step's error_code matches the response status
|
||||
|
||||
Scenario: ZIP-returning tool records OUTPUT signatures per inner PDF
|
||||
# /split-pages with multiple page numbers returns a ZIP. Stirling sends
|
||||
# application/octet-stream rather than application/zip — the extractor
|
||||
# sniffs the PK\x03\x04 magic so the ZIP unpack path still fires.
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST a 3-page PDF to "/api/v1/general/split-pages" with form fields:
|
||||
| pageNumbers | 1,2 |
|
||||
Then the response status is 200
|
||||
And exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
And the latest job has at least 2 OUTPUT artifact hashes recorded
|
||||
|
||||
Scenario: Multi-file input writes a single shadow row sized by the group
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST two single-page PDFs as a multi-file payload to "/api/v1/general/merge-pdfs"
|
||||
Then the response status is 200
|
||||
And exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
And the latest shadow charge row has payg_units >= 1
|
||||
|
||||
Scenario: PIPELINE header sets the job source
|
||||
Given there are no existing shadow charges for team "payg-cucumber-team"
|
||||
When I POST a single-page PDF with header "X-Stirling-Automation: true" to "/api/v1/security/add-password"
|
||||
Then the response status is 200
|
||||
And exactly 1 shadow charge row exists for team "payg-cucumber-team"
|
||||
And the latest job's source is "PIPELINE"
|
||||
Reference in New Issue
Block a user