diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 0000000..964d1e7 --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,33 @@ +name: install + +on: + push: + branches: [development] + pull_request: + branches: [development] + workflow_dispatch: + +jobs: + install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - name: Configure npm global prefix + run: | + npm config set prefix "$HOME/.npm-global" + echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH" + - run: npm install + - name: Pack CLI + id: pack + run: | + TARBALL=$(npm pack | tail -1 | tr -d '\r') + echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT" + - name: Install packed CLI globally + run: npm install -g "./${{ steps.pack.outputs.tarball }}" + - name: Exercise CLI + run: bp-run --help + - name: Cleanup global install + run: npm uninstall -g @theprofs/bp-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b82dce3..c18e437 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,6 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 - run: npm install - run: npm test diff --git a/README.md b/README.md index 721262d..18ec074 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ for await (const user of users()) { for await (const user of users()) { await user.set({ - stripe_subsription_status: user.subscription?.status || null + stripe_subscription_status: user.subscription?.status || null }).save() } ```` diff --git a/dist/cli.js b/dist/cli.js old mode 100644 new mode 100755 index 7e2b639..2bf56dc --- a/dist/cli.js +++ b/dist/cli.js @@ -48,7 +48,7 @@ class User { if (!changed.length) return this; log.info('Saving user $1 ($2)', [this.id, changed.join(', ')]); - const ident = (s) => `"${String(s).replace(/\"/g, '""')}"`; + const ident = (s) => `"${String(s).replace(/"/g, '""')}"`; const sets = changed.map((key, i) => `${ident(key)} = $${i + 1}`).join(', '); const values = changed.map(key => this[key]); await this._db.query(`UPDATE users SET ${sets} WHERE id = $${changed.length + 1}`, [...values, this.id]); @@ -78,7 +78,7 @@ const setKeychain = async (key, value, run = execFileDefault) => { if (!/^[A-Z_]+$/.test(key)) throw new TypeError('Key must be uppercase letters and underscores only'); try { - await run('/usr/bin/security', ['add-generic-password', '-s', 'BP_CLI', '-a', key, '-w', value]); + await run('/usr/bin/security', ['add-generic-password', '-U', '-s', 'BP_CLI', '-a', key, '-w', value]); } catch (err) { const stderr = err?.stderr?.toString() ?? ''; @@ -184,14 +184,14 @@ const discoverStripeResources = (stripe) => { }; const help = () => { console.error(`${color('bold', 'Usage:')} - bp-sync Show this help - bp-sync -h, --help Show this help - bp-sync init Create mapping.js - bp-sync exec Execute script + bp-run Show this help + bp-run -h, --help Show this help + bp-run init Create mapping.js + bp-run exec Execute script ${color('bold', 'Examples:')} - bp-sync init - bp-sync exec mapping.js`); + bp-run init + bp-run exec mapping.js`); process.exit(0); }; const init = async () => { diff --git a/package-lock.json b/package-lock.json index f17a555..3d4d7be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,14 @@ "license": "MIT", "dependencies": { "pg": "^8.13.1", - "stripe": "^17.4.0" + "stripe": "^17.4.0", + "typescript": "^5.9.3" }, "bin": { - "bp-run": "src/cli.ts" - }, - "devDependencies": { - "typescript": "^5.9.3" + "bp-run": "dist/cli.js" }, "engines": { - "node": ">=23.6" + "node": ">=24" } }, "node_modules/@types/node": { @@ -449,7 +447,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 8d738e2..fc72458 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dist/cli.js" ], "engines": { - "node": ">=23.6" + "node": ">=24" }, "scripts": { "build": "tsc src/cli.ts --outDir dist --module nodenext --target esnext --moduleResolution nodenext --esModuleInterop --skipLibCheck --noCheck", @@ -34,9 +34,7 @@ "license": "MIT", "dependencies": { "pg": "^8.13.1", - "stripe": "^17.4.0" - }, - "devDependencies": { + "stripe": "^17.4.0", "typescript": "^5.9.3" } } diff --git a/src/cli.ts b/src/cli.ts index 2aea211..b1d38ef 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -84,7 +84,7 @@ class User { log.info('Saving user $1 ($2)', [this.id, changed.join(', ')]) - const ident = (s: string) => `"${String(s).replace(/\"/g, '""')}"` + const ident = (s: string) => `"${String(s).replace(/"/g, '""')}"` const sets = changed.map((key, i) => `${ident(key)} = $${i + 1}`).join(', ') const values = changed.map(key => this[key]) @@ -136,7 +136,7 @@ const setKeychain = async ( try { await run( '/usr/bin/security', - ['add-generic-password', '-s', 'BP_CLI', '-a', key, '-w', value] + ['add-generic-password', '-U', '-s', 'BP_CLI', '-a', key, '-w', value] ) } catch (err: unknown) { const stderr = (err as any)?.stderr?.toString() ?? '' @@ -283,14 +283,14 @@ const discoverStripeResources = (stripe: Stripe): Record => { const help = (): void => { console.error(`${color('bold', 'Usage:')} - bp-sync Show this help - bp-sync -h, --help Show this help - bp-sync init Create mapping.js - bp-sync exec Execute script + bp-run Show this help + bp-run -h, --help Show this help + bp-run init Create mapping.js + bp-run exec Execute script ${color('bold', 'Examples:')} - bp-sync init - bp-sync exec mapping.js`) + bp-run init + bp-run exec mapping.js`) process.exit(0) } diff --git a/test/main.test.ts b/test/main.test.ts index 4d1951f..88050a7 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -9,6 +9,7 @@ import { createUsersGenerator, createQueryHelper } from '../src/cli.ts' +import { mocks } from './utils/index.ts' import type { Pool as PgPool } from 'pg' import type Stripe from 'stripe' @@ -419,7 +420,7 @@ test('#users', async t => { } as unknown as PgPool const stripe = { customers: { - retrieve: t.mock.fn(async (id: string) => ({ id, email: `${id}@stripe.com` })) + retrieve: t.mock.fn(mocks.stripe.customers.retrieve) } } as unknown as Stripe const users = createUsersGenerator(db, stripe) @@ -430,6 +431,29 @@ test('#users', async t => { assert.ok(items[0].customer) assert.strictEqual(items[0].customer.id, 'cus_1') + assert.strictEqual(items[0].customer.email, 'cus_1@example.com') + }) + + await t.test('returns full customer data with mock utilities', async t => { + const db = { + query: t.mock.fn(async () => ({ + rows: [{ id: 1, email: 'a@test.com', stripe_id: 'cus_test' }] + })) + } as unknown as PgPool + + t.mock.method(mocks.stripe.customers, 'retrieve') + + const users = createUsersGenerator(db, mocks.stripe as unknown as Stripe) + + const items = [] + for await (const user of users('', [], { fetchCustomer: true })) + items.push(user) + + assert.ok(items[0].customer) + assert.strictEqual(items[0].customer.id, 'cus_test') + assert.strictEqual(items[0].customer.name, 'Test Customer') + assert.strictEqual(items[0].customer.currency, 'usd') + assert.strictEqual(mocks.stripe.customers.retrieve.mock.callCount(), 1) }) await t.test('handles missing stripe_id', async t => { diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 0000000..b11c14b --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1,5 @@ +import * as stripeUtils from './stripe.ts' + +export const mocks = { + stripe: stripeUtils.stripe +} diff --git a/test/utils/stripe.ts b/test/utils/stripe.ts new file mode 100644 index 0000000..5fb4124 --- /dev/null +++ b/test/utils/stripe.ts @@ -0,0 +1,62 @@ +export const stripe = { + customers: { + retrieve: async (id: string) => ({ + id, + email: `${id}@example.com`, + name: 'Test Customer', + created: Math.floor(Date.now() / 1000), + currency: 'usd', + default_source: null, + delinquent: false, + description: null, + discount: null, + invoice_prefix: null, + invoice_settings: { default_payment_method: null }, + livemode: false, + metadata: {}, + shipping: null, + tax_exempt: 'none' + }), + + list: async (params?: any) => ({ + object: 'list', + data: [], + has_more: false, + url: '/v1/customers' + }) + }, + + subscriptions: { + list: async (params?: any) => ({ + object: 'list', + data: [], + has_more: false, + url: '/v1/subscriptions' + }), + + cancel: async (id: string) => ({ + id, + object: 'subscription', + status: 'canceled', + canceled_at: Math.floor(Date.now() / 1000) + }) + }, + + invoices: { + list: async (params?: any) => ({ + object: 'list', + data: [], + has_more: false, + url: '/v1/invoices' + }) + }, + + charges: { + list: async (params?: any) => ({ + object: 'list', + data: [], + has_more: false, + url: '/v1/charges' + }) + } +}