test(usage): prove product usage works on PostgreSQL, and harden the collector default

Everything about this feature had been exercised on SQLite only, which is
the engine least likely to show its problems.

Adds __tests__/integration/productUsagePg.test.js, following the gated
pattern the .picpeak restore suites use: it runs the real migrations
201-203 against a real PostgreSQL and covers what SQLite cannot answer.
node-postgres returns bigint as a STRING, and the withdrawal guard
compares `cancel_seq` — a `'1' !== 1` slip there would let an activation
complete after an opt-out, and SQLite, which hands back a number, would
never show it. Booleans are real booleans rather than 0/1, which is what
every `configured` signal in a report is built from. And markUsed takes
SELECT ... FOR UPDATE on this engine only.

Seven cases, all passing against PostgreSQL 15. Removing the
compare-and-swap condition fails the withdrawal case there too, so the
suite has teeth on that engine and not only on SQLite. CI already
provides PICPEAK_PG_TEST_URL, so these run there rather than skipping.

The collector default is harder to lose. An unset, empty or
whitespace-only USAGE_COLLECTOR_URL now falls back to
https://usage.picpeak.app — deployments that template the variable in
(docker-compose writes ${USAGE_COLLECTOR_URL:-...}) can hand over an
empty string, and that has to mean "use the default" rather than "no
collector". A value that is present but malformed is still reported as a
configuration error instead of being silently replaced: quietly
retargeting a self-hoster's collector at ours would send their reports
somewhere they did not choose.

Refs #1110
This commit is contained in:
Paul Nothaft
2026-09-05 22:53:57 +02:00
parent 75ef137b7d
commit c7cedb00d6
3 changed files with 198 additions and 2 deletions
+14
View File
@@ -23,6 +23,20 @@ tracker or CORS policy is required. The collector is the separate
| `USAGE_COLLECTOR_URL` | https://usage.picpeak.app | Fixed operator-configured collector origin, HTTPS in production |
| `USAGE_ENCRYPTION_KEY` | JWT_SECRET | 32+ characters, encrypts the local Ed25519 key with AES-256-GCM |
Both database engines are supported. The state and marker tables are created
by migrations 201-203 on PostgreSQL and SQLite alike, and the engine-sensitive
paths are covered by `__tests__/integration/productUsagePg.test.js` against a
real PostgreSQL — bigint columns come back as strings there, booleans are real
booleans rather than 0/1, and the marker write takes `SELECT ... FOR UPDATE`
only on that engine. That suite is gated behind `PICPEAK_PG_TEST_URL` and runs
in CI, which provides one.
If `USAGE_COLLECTOR_URL` is unset, empty or blank the built-in default
`https://usage.picpeak.app` is used. A value that is present but malformed is
reported as a configuration error rather than being replaced by the default:
silently retargeting a self-hosted collector at ours would send reports
somewhere the operator did not choose.
Local development can use an HTTP loopback collector outside production. The
collector URL is never writable through generic settings or request payloads.
Keep the encryption material stable and protected; losing it makes the old