mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 18:44:05 +02:00
## What this PR does Bundles the **low-risk polish items** from the [multi-agent review of #6519](https://github.com/Stirling-Tools/Stirling-PDF/pull/6519). Each change is independent, mechanical, and ships with focused unit-test coverage. The medium-severity items (\`?async=true\` OUTPUT recording, JSON-consumes endpoint coverage, SpringBootTest harness) are tracked separately in [\`notes/PAYG_DESIGN.md\` §7.5 PR-S4](https://github.com/Stirling-Tools/Stirling-PDF/blob/payg-s4-hardening/notes/PAYG_DESIGN.md) — they need design decisions + bigger infrastructure work, so this PR sticks to the mechanical wins. Stacked on #6519. When that merges to main, this rebases cleanly — no code changes. ## Changes | Area | What | Why | |---|---|---| | **\`tool_id\` becomes route pattern** | \`PaygChargeInterceptor.resolveToolId()\` prefers \`HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE\` over \`request.getRequestURI()\`. Truncates to 128 + WARN log + \`payg.filter.errors\` increment when truncation fires. | Audit rollups aggregate by endpoint instead of by every individual request's path-variable / matrix-param variant. Silent truncation now louder. | | **Direct PDF magic-byte check** | \`PaygOutputExtractor.extract()\` magic-checks the body even for direct \`application/pdf\` responses. | Asymmetric with the ZIP-entry path which always magic-checks. A tool that emits \`application/pdf\` for a JSON / HTML payload would otherwise pollute \`job_artifact_hash\`. | | **DESKTOP_APP detection** | \`X-Stirling-Client: desktop\` header → \`JobSource.DESKTOP_APP\`. | The enum value was unreachable from \`determineSource()\`; Tauri shell traffic was mis-classified as WEB. No anti-spoof — V12 step limits are identical for WEB/DESKTOP_APP so the worst-case abuse value is zero today. | | **\`max-bytes\` sensible default** | 500 MiB instead of \`null\` (unbounded). | Covers the largest realistic Stirling responses (full split-to-ZIP on a 1000-page document) while preventing pathological cases from tying up the interceptor for minutes. Set to \`null\` to disable. | | **\`BufferedOutputStream\` for spill** | Wraps the spill \`OutputStream\` in 64 KiB \`BufferedOutputStream\`. | Previously every Tomcat chunk (default 8 KiB) was a separate syscall. Big spilled responses get a syscall-bound speedup. | | **Duration timer per phase** | \`payg.filter.duration\` tagged \`phase=preHandle\` vs \`phase=afterCompletion\`. | Two distinct latency distributions were blended into one histogram; hard to alert on. | ## Tests | Test | What it covers | |---|---| | \`PaygOutputExtractorTest.pdfContentType_butBodyMissingPdfMagic_returnsEmpty\` | New direct-PDF magic-byte gate. | | \`PaygChargeInterceptorTest.preHandle_desktopClientHeader_setsJobSourceDesktopApp\` | New \`X-Stirling-Client: desktop\` → DESKTOP_APP path. | | \`PaygChargeInterceptorTest.preHandle_toolId_prefersBestMatchingPattern\` | Route pattern wins over URI when both are set. | | \`PaygChargeInterceptorTest.preHandle_toolId_truncatesAndCountsWhenLongerThan128\` | Oversized values truncate + increment errors counter. | Full saas suite green (210 tests), coverage targets met. ## What's NOT in this PR (deliberately) - **\`?async=true\` OUTPUT recording.** The JobExecutorService returns a synchronous \`JobResponse{jobId}\` body before the async tool actually runs; \`afterCompletion\` fires too early. Needs a design decision: short-circuit PAYG when \`async=true\` OR hook into \`TaskManager\` completion. Tracked in PR-S4 design doc. - **JSON-consumes endpoint coverage.** The \`MultipartHttpServletRequest\` cast skips endpoints with \`consumes = APPLICATION_JSON_VALUE\` (e.g. \`ConvertPdfJsonController.exportPartialPdf\`). Fix is either extract a request-body hash for JSON or add a CI lint forbidding non-multipart \`@AutoJobPostMapping\`. Design discussion needed. - **SpringBootTest harness for filter + interceptor wiring.** Saas module doesn't have one yet. Separate work — PR-S3 takes a different approach (docker-compose + Behave); a SpringBootTest layer would be additive in-process coverage. These are tracked in \`notes/PAYG_DESIGN.md §7.5\` so they don't slip. ## Tracked in \`notes/PAYG_DESIGN.md\` §7.5 PR-S4.
124 lines
5.7 KiB
SQL
124 lines
5.7 KiB
SQL
-- Seed test team / payg policy / wallet_policy in PAYG_SHADOW mode for the
|
|
-- cucumber harness. Piped through psql AFTER Hibernate has built the schema
|
|
-- on backend startup (compose disables Flyway — see docker-compose-saas.yml
|
|
-- for the rationale: saas Flyway migrations assume `users` + `teams` exist
|
|
-- because Supabase normally provisions them).
|
|
--
|
|
-- Idempotent — guarded against duplicate keys so re-running on the same
|
|
-- container is safe between scenarios.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 0. Default pricing policy + per-source step limits (V12 in production).
|
|
-- Required because PricingPolicyService.getEffectivePolicy() throws if
|
|
-- no row has is_default = TRUE. Explicit timestamps because Hibernate's
|
|
-- @CreationTimestamp is application-side; direct INSERTs bypass it.
|
|
-- ---------------------------------------------------------------------------
|
|
-- NOTE: free_tier_units_per_cycle is supplied explicitly because the cucumber
|
|
-- harness disables Flyway (see docker-compose-saas.yml). Hibernate's DDL
|
|
-- emits NOT NULL without a SQL DEFAULT (the JPA field default `= 0L` is
|
|
-- JVM-side only), so the column would otherwise reject this INSERT.
|
|
-- 500 matches the launch free-tier (PAYG_DESIGN §3.10 revised).
|
|
INSERT INTO stirling_pdf.pricing_policy (
|
|
version, effective_from, doc_pages_per_unit, doc_bytes_per_unit,
|
|
min_charge_units, file_unit_cap, free_tier_units_per_cycle, is_default,
|
|
notes, created_by, created_at
|
|
)
|
|
SELECT
|
|
'v1-cucumber', CURRENT_TIMESTAMP, 25, 5242880,
|
|
1, 1000, 500, TRUE,
|
|
'Cucumber test default policy', 'system',
|
|
CURRENT_TIMESTAMP
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM stirling_pdf.pricing_policy WHERE is_default = TRUE
|
|
);
|
|
|
|
INSERT INTO stirling_pdf.pricing_policy_step_limit (policy_id, job_source, step_limit)
|
|
SELECT p.policy_id, src.job_source, src.step_limit
|
|
FROM stirling_pdf.pricing_policy p
|
|
CROSS JOIN (
|
|
VALUES
|
|
('WEB', 10),
|
|
('API', 10),
|
|
('PIPELINE', 20),
|
|
('DESKTOP_APP', 10)
|
|
) AS src(job_source, step_limit)
|
|
WHERE p.is_default = TRUE
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM stirling_pdf.pricing_policy_step_limit s
|
|
WHERE s.policy_id = p.policy_id AND s.job_source = src.job_source
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 1. Test team. The Stirling-PDF backend auto-creates `Default` and
|
|
-- `Internal` teams at boot; we add a third one specifically for the
|
|
-- PAYG scenarios so we can isolate state and assert per-team.
|
|
-- ---------------------------------------------------------------------------
|
|
INSERT INTO stirling_pdf.teams (name)
|
|
SELECT 'payg-cucumber-team'
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM stirling_pdf.teams WHERE name = 'payg-cucumber-team'
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 2. payg_team_extensions sidecar — uses the default pricing policy.
|
|
-- ---------------------------------------------------------------------------
|
|
INSERT INTO stirling_pdf.payg_team_extensions (team_id, pricing_policy_id, stripe_customer_id)
|
|
SELECT t.team_id, NULL, NULL
|
|
FROM stirling_pdf.teams t
|
|
WHERE t.name = 'payg-cucumber-team'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM stirling_pdf.payg_team_extensions ext WHERE ext.team_id = t.team_id
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 3. Bind the auto-created CUSTOM_API_USER to our cucumber team. The user
|
|
-- is created by the backend's SECURITY_CUSTOMGLOBALAPIKEY handling — we
|
|
-- don't seed our own user (would collide on the unique api_key).
|
|
-- Update their primary `team_id` so JobChargeService picks up
|
|
-- payg-cucumber-team as the owner team on requests.
|
|
-- ---------------------------------------------------------------------------
|
|
UPDATE stirling_pdf.users u
|
|
SET team_id = (SELECT team_id FROM stirling_pdf.teams WHERE name = 'payg-cucumber-team')
|
|
WHERE u.username = 'CUSTOM_API_USER';
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 4. Team membership row so /teams/* admin paths recognise the user.
|
|
-- All timestamp columns set explicitly because Hibernate-generated DDL
|
|
-- doesn't carry the Flyway-migration DEFAULT CURRENT_TIMESTAMP.
|
|
-- ---------------------------------------------------------------------------
|
|
INSERT INTO stirling_pdf.team_memberships (
|
|
team_id, user_id, role, invited_at, created_at, updated_at
|
|
)
|
|
SELECT t.team_id, u.user_id, 'LEADER',
|
|
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
FROM stirling_pdf.teams t, stirling_pdf.users u
|
|
WHERE t.name = 'payg-cucumber-team'
|
|
AND u.username = 'CUSTOM_API_USER'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM stirling_pdf.team_memberships m
|
|
WHERE m.team_id = t.team_id AND m.user_id = u.user_id
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 5. wallet_policy in PAYG_SHADOW mode.
|
|
-- auto_group_strategy is the dead column tracked for drop in PR-R11
|
|
-- (PAYG_DESIGN.md §7.7) — until that ships, the NOT NULL constraint
|
|
-- means we have to populate it. 'AUTO' is the prod default.
|
|
-- ---------------------------------------------------------------------------
|
|
INSERT INTO stirling_pdf.wallet_policy (
|
|
team_id, engine, cap_period, warn_at_pct, degrade_at_pct,
|
|
degraded_feature_set, auto_group_strategy, notification_emails,
|
|
updated_at
|
|
)
|
|
SELECT t.team_id, 'PAYG_SHADOW', 'CALENDAR_MONTH', 80, 100,
|
|
'MINIMAL', 'AUTO', '[]'::jsonb, CURRENT_TIMESTAMP
|
|
FROM stirling_pdf.teams t
|
|
WHERE t.name = 'payg-cucumber-team'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM stirling_pdf.wallet_policy wp WHERE wp.team_id = t.team_id
|
|
);
|
|
|
|
UPDATE stirling_pdf.wallet_policy
|
|
SET engine = 'PAYG_SHADOW', updated_at = CURRENT_TIMESTAMP
|
|
WHERE team_id = (SELECT team_id FROM stirling_pdf.teams WHERE name = 'payg-cucumber-team');
|