mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 08:16:57 +00:00
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
55 lines
2.4 KiB
JavaScript
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(() => {});
|