Files
at-container-registry/test/e2e/batch13-webhook-downgrade.mjs
T
Evan JarrettandClaude Opus 5 18c77ace28 test/e2e: drive the billing Drive list against the Stripe sandbox
Three scripts covering what only a browser can see, run end to end against the
sandbox with a real checkout and a real portal cancellation.

batch13-billing-drive.mjs runs the same account either side of one config
change -- whether its default hold appears in server.managed_holds. Managed:
the billing tab offers real tiers, checkout 302s to Stripe, the portal is
reachable. Self-hosted: checkout 403s, the portal still 302s, and the advisor
answers managed_hold_required rather than upgrade_required, which matters
because telling a paying subscriber to "upgrade" would sell them a tier they
already hold.

batch13-portal-cancel.mjs walks into Stripe's portal instead of asserting the
redirect, because the batch card calls a subscriber who cannot cancel the worst
outcome here and a 302 does not prove a cancel control exists at the far end.

batch13-webhook-downgrade.mjs creates three webhooks under an allowance of ten,
then reads the page back after the downgrade.

Every one of these is invisible on a hold owner's account: GetSubscriptionInfo
returns a synthetic "Captain" tier before any Stripe lookup, and
GetWebhookLimits / HasAIAdvisor / GetSupporterBadge bypass on the same first
line. The first run used the shared e2e profile, which still had the owner
signed in, and reported a clean pass built entirely on that bypass. The scripts
now assert the page does not render "Captain", and take a separate profile.

Two traps worth keeping: a bare button[type=submit] matches the nav's hidden
logout button before the form's own submit, and hx-confirm here renders a
custom modal whose backdrop swallows clicks -- strip the attribute rather than
trying to dismiss a dialog that never fires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxF2N3HuZ8xSkx6nkirgB
2026-08-25 16:34:26 -05:00

55 lines
2.4 KiB
JavaScript

// batch13-webhook-downgrade.mjs — the last item on the billing Drive list.
//
// 6510c16 introduced a gap between "listed" and "delivered": dispatch caps a
// user's webhooks to their CURRENT allowance, while the settings page keeps
// showing every webhook they created. This drives the gap: create three while
// entitled to ten, cancel the subscription through Stripe's real portal, and
// confirm all three are still listed under an allowance of one.
//
// The delivery half is not observable here — it fires from DispatchForScan and
// the dev stack runs no scanner. It is unit-covered by 33c2321's own
// dispatch_entitlement_test.go.
import { open, reporter, APPVIEW } from './lib.mjs';
const { record, summarize } = reporter();
const ctx = await open();
const page = ctx.pages()[0] ?? (await ctx.newPage());
const text = () => page.evaluate(() => document.body.innerText);
const allowance = (s) => (s.match(/(\d+)\s*\/\s*(\d+)\s*webhooks configured/) ?? []).slice(1, 3);
await page.goto(`${APPVIEW}/settings/webhooks`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
let hooks = await text();
console.log('before:', allowance(hooks).join(' of '), 'configured\n');
// Create three webhooks through the form the user would use.
for (const n of [1, 2, 3]) {
const url = `https://example.invalid/hook-${n}`;
const already = (await text()).includes(url);
if (already) { console.log(`hook-${n} already present`); continue; }
// Scope to the webhook form: a bare button[type=submit] also matches the
// nav's hidden logout button, which is the first match in the DOM.
const form = page.locator('form[hx-post="/api/webhooks"]');
await form.locator('input[name="url"]').fill(url);
await form.locator('button[type="submit"]').click();
await page.waitForTimeout(2000);
console.log(`created hook-${n}`);
}
await page.goto(`${APPVIEW}/settings/webhooks`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1500);
hooks = await text();
const [have, cap] = allowance(hooks);
console.log(`after creating: ${have} of ${cap} configured\n`);
record(
'three webhooks exist while entitled',
Number(have) >= 3,
`only ${have} present; the downgrade check needs three`,
);
record('the allowance is the paid one', Number(cap) >= 3, `cap is ${cap}, expected the Pro allowance`);
console.log(hooks.slice(0, 1400));
summarize();
console.log('\nLeaving the browser open for the portal step.');
await new Promise(() => {});