mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
250 lines
18 KiB
JavaScript
250 lines
18 KiB
JavaScript
const { URL } = require("url");
|
|
const { expect } = require("chai");
|
|
require("ts-node/register/transpile-only");
|
|
const mongoose = require("mongoose");
|
|
const config = require("../src/config").default;
|
|
const { BillingPrice, BillingAccount, BillingGrant, BillingEvent, BillingRevocation, BillingLock, BillingDraft } = require("../src/core/billing/models");
|
|
const { receiveBillingEvent, processBillingInbox, billingHealth } = require("../src/core/billing/events");
|
|
const { reconcileBillingAccount } = require("../src/core/billing/reconciliation");
|
|
const { createCheckout, processBillingEvent } = require("../src/core/billing/stripe");
|
|
const { reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
|
|
const Repo = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
|
|
const uri = process.env.BILLING_TEST_MONGODB_URI;
|
|
const owner = "507f1f77bcf86cd799439011";
|
|
const { saveBillingDraft, getBillingDraft } = require("../src/core/billing/drafts");
|
|
const models = [BillingDraft, BillingPrice, BillingAccount, BillingGrant, BillingEvent, BillingRevocation, BillingLock, Repo];
|
|
const iterable = values => ({ async *[Symbol.asyncIterator]() { yield* values; } });
|
|
const event = id => ({ id, livemode: false, type: "customer.created", data: { object: { id: "cus_test", email: "private@example.org" } } });
|
|
(uri ? describe : describe.skip)("billing failure recovery", function () {
|
|
this.timeout(20000);
|
|
const original = { ...config };
|
|
before(async () => {
|
|
if (!/^billing_test_/.test(new URL(uri).pathname.slice(1))) throw new Error("Disposable database required");
|
|
await mongoose.connect(uri);
|
|
await Promise.all(models.map(m => m.init()));
|
|
});
|
|
beforeEach(async () => {
|
|
await Promise.all(models.map(m => m.deleteMany({})));
|
|
Object.assign(config, { BILLING_ENABLED: true, BILLING_CHECKOUT_ENABLED: true, STRIPE_AUTHOR_MONTHLY_PRICE: "price_monthly" });
|
|
await BillingAccount.create({ _id: owner, customerId: "cus_owner" });
|
|
});
|
|
after(async () => { Object.assign(config, original); await mongoose.disconnect(); });
|
|
it("deduplicates concurrent receipt without retaining customer data", async () => {
|
|
await Promise.all(Array.from({ length: 8 }, () => receiveBillingEvent(event("evt_one"))));
|
|
expect(await BillingEvent.countDocuments()).to.equal(1);
|
|
expect(JSON.stringify(await BillingEvent.findOne())).not.to.include("private@example.org");
|
|
let calls = 0;
|
|
await Promise.all([processBillingInbox(1, async () => { calls++; }), processBillingInbox(1, async () => { calls++; })]);
|
|
expect(calls).to.equal(1);
|
|
expect((await BillingEvent.findOne()).state).to.equal("complete");
|
|
});
|
|
it("recovers a crash after side effects but before acknowledgement", async () => {
|
|
await receiveBillingEvent(event("evt_crash"));
|
|
await BillingGrant.create({ _id: "already-applied", status: "paid" });
|
|
await BillingEvent.updateOne({}, { $set: { state: "processing", token: "dead-worker", leaseUntil: new Date(0) } });
|
|
await processBillingInbox(1, async () => {
|
|
await BillingGrant.updateOne({ _id: "already-applied" }, { $setOnInsert: { status: "paid" } }, { upsert: true });
|
|
});
|
|
expect(await BillingGrant.countDocuments()).to.equal(1);
|
|
expect((await BillingEvent.findOne()).state).to.equal("complete");
|
|
});
|
|
it("backs off failures, redacts errors and exposes exhausted retries", async () => {
|
|
await receiveBillingEvent(event("evt_fail"));
|
|
await processBillingInbox(1, async () => { throw new Error("secret payload"); });
|
|
let record = await BillingEvent.findOne();
|
|
expect(record.state).to.equal("pending");
|
|
expect(record.nextAttemptAt.getTime()).to.be.greaterThan(record.receivedAt.getTime());
|
|
expect(record.lastError).not.to.include("secret");
|
|
await BillingEvent.updateOne({}, { $set: { attempts: 11, nextAttemptAt: new Date(0) } });
|
|
await processBillingInbox(1, async () => { throw new Error("secret"); });
|
|
record = await BillingEvent.findOne();
|
|
expect(record.state).to.equal("failed");
|
|
expect((await billingHealth()).failed).to.equal(1);
|
|
});
|
|
it("recovers missing grants from provider history and applies missed refunds", async () => {
|
|
const sub = { id: "sub_lost", metadata: { application: "anonymous-github", owner }, customer: "cus_owner", status: "active",
|
|
latest_invoice: { status: "paid" }, items: { data: [{ price: { id: "price_monthly" }, current_period_end: Math.floor(Date.now() / 1000) + 86400 }] } };
|
|
const stripe = { subscriptions: { list: () => iterable([sub]), retrieve: async () => sub },
|
|
checkout: { sessions: { list: () => iterable([]) } },
|
|
charges: { list: () => iterable([{ id: "ch_refunded", refunded: true, payment_intent: "pi_refunded" }]) },
|
|
invoicePayments: { list: async () => ({ data: [{ invoice: "in_refunded" }] }) },
|
|
invoices: { retrieve: async () => ({ parent: { subscription_details: { subscription: "sub_lost" } } }) } };
|
|
await reconcileBillingAccount(owner, stripe);
|
|
expect((await BillingGrant.findById("sub_lost")).revoked).to.equal(true);
|
|
expect((await BillingAccount.findById(owner)).reconciledAt).to.be.instanceOf(Date);
|
|
// A restored database with no grants can reconstruct the same final state.
|
|
await BillingGrant.deleteMany({});
|
|
await reconcileBillingAccount(owner, stripe);
|
|
expect((await BillingGrant.findById("sub_lost")).revoked).to.equal(true);
|
|
});
|
|
it("recovers paid time when the latest invoice failed after a missed renewal", async () => {
|
|
const { syncSubscription } = require("../src/core/billing/stripe");
|
|
const end = Math.floor(Date.now() / 1000);
|
|
await syncSubscription("sub_history", {
|
|
subscriptions: { retrieve: async () => ({ id: "sub_history", customer: "cus_owner", metadata: { application: "anonymous-github", owner },
|
|
status: "past_due", latest_invoice: { status: "open" }, items: { data: [{ id: "si_owned", price: { id: "price_monthly" }, current_period_end: end + 86400 }] } }) },
|
|
invoices: { list: () => iterable([{ id: "in_paid" }]), listLineItems: () => iterable([
|
|
{ amount: 500, parent: { subscription_item_details: { subscription_item: "si_owned" } }, period: { end } },
|
|
{ amount: 500, parent: { subscription_item_details: { subscription_item: "si_unrelated" } }, period: { end: end + 999999 } },
|
|
]) },
|
|
});
|
|
expect((await BillingGrant.findById("sub_history")).until.getTime()).to.equal(end * 1000);
|
|
});
|
|
it("preserves a refund received before the first grant exists", async () => {
|
|
const { fulfillCheckout } = require("../src/core/billing/stripe");
|
|
config.STRIPE_AUTHOR_PASS_PRICE = "price_pass";
|
|
const stripe = { invoicePayments: { list: async () => ({ data: [] }) }, checkout: { sessions: { retrieve: async () => ({
|
|
id: "cs_late", status: "complete", payment_status: "paid", customer: "cus_owner", created: Math.floor(Date.now() / 1000),
|
|
metadata: { application: "anonymous-github", owner, planId: "author_pass", target: "repo" },
|
|
line_items: { data: [{ price: { id: "price_pass" }, quantity: 1 }] },
|
|
payment_intent: { id: "pi_early", latest_charge: { refunded: false, disputed: false } },
|
|
}) } } };
|
|
await processBillingEvent({ type: "charge.refunded", data: { object: { payment_intent: "pi_early", refunded: true } } }, stripe);
|
|
await fulfillCheckout("cs_late", stripe);
|
|
expect((await BillingGrant.findById("cs_late")).revoked).to.equal(true);
|
|
});
|
|
it("starts a fresh attempt only after the old immutable request cannot execute", async () => {
|
|
await BillingAccount.updateOne({ _id: owner }, { $set: { checkout: {
|
|
key: "old", planId: "author_monthly", target: "", createdAt: new Date(0), params: { expires_at: 1000 },
|
|
} } });
|
|
const stripe = checkoutStripe();
|
|
stripe.checkout.sessions.create = async (_params, options) => {
|
|
expect(options.idempotencyKey).not.to.equal("old");
|
|
return { id: "cs_fresh", url: "https://checkout.stripe.com/fresh" };
|
|
};
|
|
expect(await createCheckout(owner, "author_monthly", "", stripe)).to.equal("https://checkout.stripe.com/fresh");
|
|
});
|
|
it("keeps partial refunds active and revokes a full refund", async () => {
|
|
await BillingGrant.create({ _id: "pass", paymentIntentId: "pi_pass", status: "paid" });
|
|
const stripe = { invoicePayments: { list: async () => ({ data: [] }) } };
|
|
const refund = refunded => ({ type: "charge.refunded", data: { object: { payment_intent: "pi_pass", refunded } } });
|
|
await processBillingEvent(refund(false), stripe);
|
|
expect((await BillingGrant.findById("pass")).revoked).to.equal(false);
|
|
await processBillingEvent(refund(true), stripe);
|
|
expect((await BillingGrant.findById("pass")).revocationReason).to.equal("full_refund");
|
|
});
|
|
it("requires review for a new dispute but ignores an older refund replay after review", async () => {
|
|
await BillingGrant.create({ _id: "reviewed-pass", paymentIntentId: "pi_reviewed", status: "paid" });
|
|
const stripe = { invoicePayments: { list: async () => ({ data: [] }) }, charges: { retrieve: async () => ({ payment_intent: "pi_reviewed", disputed: true }) } };
|
|
const refund = { type: "charge.refunded", data: { object: { payment_intent: "pi_reviewed", refunded: true } } };
|
|
await processBillingEvent(refund, stripe);
|
|
await BillingRevocation.updateOne({ _id: "pi_reviewed" }, { $set: { reviewed: true } });
|
|
await BillingGrant.updateOne({ _id: "reviewed-pass" }, { $set: { revoked: false } });
|
|
await processBillingEvent({ type: "charge.dispute.created", data: { object: { charge: "ch_reviewed" } } }, stripe);
|
|
expect((await BillingGrant.findById("reviewed-pass")).revoked).to.equal(true);
|
|
await BillingRevocation.updateOne({ _id: "pi_reviewed" }, { $set: { reviewed: true } });
|
|
await BillingGrant.updateOne({ _id: "reviewed-pass" }, { $set: { revoked: false } });
|
|
await processBillingEvent(refund, stripe);
|
|
expect((await BillingGrant.findById("reviewed-pass")).revoked).to.equal(false);
|
|
});
|
|
it("defers expiration during outage and discovers a renewal before removing content", async () => {
|
|
const deadline = new Date(Date.now() - 1000);
|
|
await Repo.create({ repoId: "protected", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: deadline });
|
|
await reconcileNeverExpiration(new Date(), async () => { throw new Error("Stripe unavailable"); });
|
|
expect((await Repo.findOne()).options.expirationMode).to.equal("never");
|
|
const until = new Date(Date.now() + 86400_000);
|
|
await reconcileNeverExpiration(new Date(), async () => {
|
|
await BillingGrant.create({ _id: "renewal", owner, scope: `user:${owner}`, planId: "author_monthly", until, status: "active" });
|
|
});
|
|
const repo = await Repo.findOne();
|
|
expect(repo.options.expirationMode).to.equal("never");
|
|
expect(repo.billingNeverExpiresUntil.getTime()).to.equal(until.getTime() + NEVER_EXPIRATION_GRACE_MS);
|
|
});
|
|
function checkoutStripe() {
|
|
return { prices: { retrieve: async () => ({ active: true, currency: "eur", unit_amount: 500, recurring: { interval: "month", interval_count: 1 } }) },
|
|
subscriptions: { list: async () => ({ data: [] }) }, checkout: { sessions: {
|
|
create: async () => { throw new Error("connection lost"); },
|
|
list: () => iterable([]),
|
|
retrieve: async () => ({ status: "open", url: "https://checkout.stripe.com/recovered" }),
|
|
} } };
|
|
}
|
|
it("records an accepted one-time price before an uncertain checkout response", async () => {
|
|
config.STRIPE_AUTHOR_PASS_PRICE = "price_retiring";
|
|
await Repo.create({ repoId: "pass-target", owner, status: "ready" });
|
|
const stripe = checkoutStripe();
|
|
stripe.prices.retrieve = async () => ({ active: true, currency: "eur", unit_amount: 1500 });
|
|
try { await createCheckout(owner, "author_pass", "pass-target", stripe); throw Error("expected network failure"); }
|
|
catch (error) { expect(error.message).to.equal("connection lost"); }
|
|
expect((await BillingPrice.findById("price_retiring")).planId).to.equal("author_pass");
|
|
config.STRIPE_AUTHOR_PASS_PRICE = "";
|
|
const { fulfillCheckout } = require("../src/core/billing/stripe");
|
|
stripe.checkout.sessions.retrieve = async () => ({ id: "cs_retired", status: "complete", payment_status: "paid", customer: "cus_owner",
|
|
created: Math.floor(Date.now() / 1000), metadata: { application: "anonymous-github", owner, planId: "author_pass", target: "pass-target" },
|
|
line_items: { data: [{ price: { id: "price_retiring" }, quantity: 1 }] }, payment_intent: null });
|
|
await fulfillCheckout("cs_retired", stripe);
|
|
expect((await BillingGrant.findById("cs_retired")).status).to.equal("paid");
|
|
});
|
|
it("keeps checkout drafts private, bounded and expired even before the TTL sweep", async () => {
|
|
const draft = await saveBillingDraft(owner, "/anonymize", { terms: "Private Author", options: { expirationMode: "never" }, customerId: "untrusted" });
|
|
expect((await getBillingDraft(owner, draft.id)).values).not.to.have.property("customerId");
|
|
for (const other of ["someone-else", owner]) {
|
|
if (other === owner) await BillingDraft.updateOne({ _id: draft.id }, { expiresAt: new Date(0) });
|
|
try { await getBillingDraft(other, draft.id); throw new Error("expected rejection"); }
|
|
catch (error) { expect(error.message).to.equal("billing_draft_not_found"); }
|
|
}
|
|
for (const path of ["//evil.example", "/anonymize?next=https://evil.example", "/anonymize/../profile"]) {
|
|
try { await saveBillingDraft(owner, path, {}); throw new Error("expected rejection"); }
|
|
catch (error) { expect(error.message).to.equal("billing_invalid_draft"); }
|
|
}
|
|
try { await saveBillingDraft(owner, "/anonymize", { terms: "x".repeat(65537) }); throw new Error("expected rejection"); }
|
|
catch (error) { expect(error.message).to.equal("billing_invalid_draft"); }
|
|
});
|
|
it("returns checkout to the saved task and refuses reuse for a different task", async () => {
|
|
const stripe = checkoutStripe();
|
|
const draft = await saveBillingDraft(owner, "/anonymize", { terms: "Author" });
|
|
const destination = `/anonymize?billingDraft=${draft.id}`;
|
|
let parameters;
|
|
stripe.checkout.sessions.create = async params => { parameters = params; return { id: "cs_task", url: "https://checkout.stripe.com/task" }; };
|
|
await createCheckout(owner, "author_monthly", "", stripe, destination);
|
|
expect(parameters.success_url).to.include(destination + "&billing=success");
|
|
expect(parameters.cancel_url).to.include(destination + "&billing=cancelled");
|
|
await createCheckout(owner, "author_monthly", "", stripe, destination);
|
|
try { await createCheckout(owner, "author_monthly", "", stripe); throw new Error("expected rejection"); }
|
|
catch (error) { expect(error.message).to.equal("billing_checkout_in_progress"); }
|
|
try { await createCheckout(owner, "author_monthly", "", stripe, "https://evil.example"); throw new Error("expected rejection"); }
|
|
catch (error) { expect(error.message).to.equal("billing_invalid_draft"); }
|
|
});
|
|
it("retries a lost response with identical parameters and the same key", async () => {
|
|
const stripe = checkoutStripe();
|
|
let first;
|
|
stripe.checkout.sessions.create = async (params, options) => { first = { params, options }; throw new Error("connection lost"); };
|
|
try { await createCheckout(owner, "author_monthly", "", stripe); } catch (e) { expect(e.message).to.equal("connection lost"); }
|
|
const saved = await BillingAccount.findById(owner);
|
|
expect(saved.checkout.id).to.equal(undefined);
|
|
stripe.checkout.sessions.create = async (params, options) => {
|
|
expect(params).to.deep.equal(first.params);
|
|
expect(options).to.deep.equal(first.options);
|
|
return { id: "cs_recovered", url: "https://checkout.stripe.com/recovered" };
|
|
};
|
|
await createCheckout(owner, "author_monthly", "", stripe);
|
|
expect((await BillingAccount.findById(owner)).checkout.id).to.equal("cs_recovered");
|
|
});
|
|
it("recovers an old attempt by metadata without creating another payment", async () => {
|
|
await BillingAccount.updateOne({ _id: owner }, { $set: { checkout: { key: "attempt", planId: "author_monthly", target: "", createdAt: new Date(0) } } });
|
|
const stripe = checkoutStripe();
|
|
stripe.checkout.sessions.list = () => iterable([{ id: "cs_recovered", metadata: { attempt: "attempt", owner } }]);
|
|
expect(await createCheckout(owner, "author_monthly", "", stripe)).to.equal("https://checkout.stripe.com/recovered");
|
|
});
|
|
it("blocks unresolved old requests instead of reusing a pruned key", async () => {
|
|
await BillingAccount.updateOne({ _id: owner }, { $set: { checkout: { key: "old", planId: "author_monthly", target: "", createdAt: new Date(0) } } });
|
|
try { await createCheckout(owner, "author_monthly", "", checkoutStripe()); throw new Error("expected rejection"); }
|
|
catch (e) { expect(e.message).to.equal("billing_recovery_required"); }
|
|
});
|
|
it("checks subsequent subscription pages before offering another subscription", async () => {
|
|
const stripe = checkoutStripe();
|
|
stripe.subscriptions.list = async ({ starting_after }) => starting_after
|
|
? { data: [{ id: "active", status: "active" }], has_more: false }
|
|
: { data: [{ id: "old", status: "canceled" }], has_more: true };
|
|
try { await createCheckout(owner, "author_monthly", "", stripe); throw new Error("expected rejection"); }
|
|
catch (e) { expect(e.message).to.equal("billing_subscription_exists"); }
|
|
});
|
|
it("stops new purchases independently of event processing", async () => {
|
|
config.BILLING_CHECKOUT_ENABLED = false;
|
|
try { await createCheckout(owner, "author_monthly", "", checkoutStripe()); throw new Error("expected rejection"); }
|
|
catch (e) { expect(e.message).to.equal("billing_checkout_disabled"); }
|
|
await receiveBillingEvent(event("evt_existing"));
|
|
await processBillingInbox(1, async () => {});
|
|
expect((await BillingEvent.findOne()).state).to.equal("complete");
|
|
});
|
|
});
|