[object Object]

← back to Animals

test: verify Stripe upgrade-checkout line items + live-key guard

2ab8a9ef17685682d8fae2047eb343b6d1001d82 · 2026-05-26 12:27:52 -0700 · SteveStudio2

Files touched

Diff

commit 2ab8a9ef17685682d8fae2047eb343b6d1001d82
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 26 12:27:52 2026 -0700

    test: verify Stripe upgrade-checkout line items + live-key guard
---
 test/lib/stripe.test.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 108 insertions(+)

diff --git a/test/lib/stripe.test.js b/test/lib/stripe.test.js
new file mode 100644
index 0000000..d6d65fa
--- /dev/null
+++ b/test/lib/stripe.test.js
@@ -0,0 +1,108 @@
+// test/lib/stripe.test.js
+//
+// Contract tests for src/lib/stripe.js — the upgrade-order Checkout builder.
+// Verifies the money-shape of the Checkout session WITHOUT hitting Stripe:
+//   - starter = $499 one-time setup + $49/mo hosting
+//   - pro     = $999 one-time setup + $49/mo hosting
+//   - mode: subscription, correct success/cancel URLs, order metadata
+//   - getStripe() refuses an sk_live_ key outside NODE_ENV=production
+//
+// Mocking strategy: set a fake sk_test_ key so getStripe() constructs a real
+// Stripe instance (the constructor makes no network call), then replace
+// stripe.checkout.sessions.create with a stub that captures its args.
+
+process.env.STRIPE_SECRET_KEY = 'sk_test_fake_for_unit_tests';
+
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+
+const { getStripe, planSpec, createUpgradeCheckoutSession, isStripeConfigured } =
+  await import('../../src/lib/stripe.js');
+
+// Stub the network-bound create() on the cached singleton.
+const stripe = getStripe();
+let captured = null;
+stripe.checkout.sessions.create = async (args) => {
+  captured = args;
+  return { id: 'cs_test_123', url: 'https://checkout.stripe.com/c/pay/cs_test_123' };
+};
+
+function lineAmounts(args) {
+  return args.line_items.map((li) => ({
+    name: li.price_data.product_data.name,
+    amount: li.price_data.unit_amount,
+    recurring: li.price_data.recurring?.interval || null,
+  }));
+}
+
+describe('stripe upgrade checkout', () => {
+  it('reports configured when a key is present', () => {
+    assert.equal(isStripeConfigured(), true);
+  });
+
+  it('planSpec exposes $499 / $999 setup fees', () => {
+    assert.equal(planSpec('starter_499').setup_cents, 49900);
+    assert.equal(planSpec('pro_999').setup_cents, 99900);
+    assert.equal(planSpec('nonsense'), null);
+  });
+
+  it('starter session = $499 setup + $49/mo hosting, subscription mode', async () => {
+    const session = await createUpgradeCheckoutSession({
+      orderId: 42, plan: 'starter_499', customerEmail: 'a@b.com',
+      businessName: 'Acme Vet', baseUrl: 'https://example.test',
+    });
+    assert.equal(session.url, 'https://checkout.stripe.com/c/pay/cs_test_123');
+    assert.equal(captured.mode, 'subscription');
+    assert.equal(captured.customer_email, 'a@b.com');
+
+    const items = lineAmounts(captured);
+    assert.equal(items.length, 2);
+    // one-time setup
+    assert.equal(items[0].amount, 49900);
+    assert.equal(items[0].recurring, null);
+    assert.match(items[0].name, /Acme Vet/);
+    // recurring hosting
+    assert.equal(items[1].amount, 4900);
+    assert.equal(items[1].recurring, 'month');
+
+    assert.match(captured.success_url, /\/upgrade\/success\?session_id=\{CHECKOUT_SESSION_ID\}$/);
+    assert.match(captured.cancel_url, /\/upgrade\/cancel\?order_id=42$/);
+    assert.equal(captured.metadata.upgrade_order_id, '42');
+    assert.equal(captured.metadata.plan, 'starter_499');
+  });
+
+  it('pro session = $999 setup + $49/mo hosting', async () => {
+    await createUpgradeCheckoutSession({
+      orderId: 7, plan: 'pro_999', customerEmail: 'c@d.com',
+      businessName: null, baseUrl: 'https://example.test',
+    });
+    const items = lineAmounts(captured);
+    assert.equal(items[0].amount, 99900);
+    assert.equal(items[1].amount, 4900);
+    assert.equal(items[1].recurring, 'month');
+  });
+
+  it('rejects an unknown plan', async () => {
+    await assert.rejects(
+      () => createUpgradeCheckoutSession({ orderId: 1, plan: 'enterprise_4999', baseUrl: 'x' }),
+      /unknown plan/,
+    );
+  });
+});
+
+describe('stripe live-key guard', () => {
+  it('refuses an sk_live_ key when NODE_ENV !== production', async () => {
+    // Fresh module instance so the cached singleton from above doesn't shadow.
+    const mod = await import('../../src/lib/stripe.js?livecheck=1');
+    const prevKey = process.env.STRIPE_SECRET_KEY;
+    const prevEnv = process.env.NODE_ENV;
+    process.env.STRIPE_SECRET_KEY = 'sk_live_should_be_refused';
+    delete process.env.NODE_ENV;
+    try {
+      assert.throws(() => mod.getStripe(), /live Stripe key outside/);
+    } finally {
+      process.env.STRIPE_SECRET_KEY = prevKey;
+      if (prevEnv !== undefined) process.env.NODE_ENV = prevEnv;
+    }
+  });
+});

← ecd4ce9 Add lost-pet alert opt-in surface (/me prefs page + signup c  ·  back to Animals  ·  Untrack committed .log files and ignore *.pre-* backups d2da3c7 →