Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/plugins/stripe.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const res = await fetch(`/api/stripe/rest`, {
// Authorization: `JWT ${token}` // NOTE: do this if not in a browser (i.e. curl or Postman)
},
body: JSON.stringify({
stripeMethod: 'stripe.subscriptions.list',
stripeMethod: 'subscriptions.list',
stripeArgs: [
{
customer: 'abc',
Expand Down
102 changes: 102 additions & 0 deletions packages/plugin-stripe/src/routes/rest.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { PayloadRequest } from 'payload'

import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockAddDataAndFileToRequest, mockStripeProxy } = vi.hoisted(() => ({
mockAddDataAndFileToRequest: vi.fn(async (_req?: unknown) => undefined),
mockStripeProxy: vi.fn(),
}))

vi.mock('payload', () => {
class Forbidden extends Error {
status = 403

constructor(_t?: unknown) {
super('Not allowed to perform this action.')
}
}

return {
addDataAndFileToRequest: mockAddDataAndFileToRequest,
Forbidden,
}
})

vi.mock('../utilities/stripeProxy.js', () => ({
stripeProxy: mockStripeProxy,
}))

import { stripeREST } from './rest.js'

const createRequest = ({ user }: { user?: { id: string } } = {}) => {
const logger = { error: vi.fn() }
const req = {
data: {
stripeArgs: [{ limit: 2 }],
stripeMethod: 'subscriptions.list',
},
payload: { logger },
t: vi.fn(),
user,
} as unknown as PayloadRequest

return { logger, req }
}

const pluginConfig = {
stripeSecretKey: 'sk_test_example',
}

describe('stripeREST', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('should return the Stripe proxy response for an authenticated request', async () => {
mockStripeProxy.mockResolvedValue({ data: { id: 'sub_123' }, status: 200 })
const { logger, req } = createRequest({ user: { id: 'user_123' } })

const response = await stripeREST({ pluginConfig, req })

expect(mockAddDataAndFileToRequest).toHaveBeenCalledWith(req)
expect(mockStripeProxy).toHaveBeenCalledWith({
stripeArgs: [{ limit: 2 }],
stripeMethod: 'subscriptions.list',
stripeSecretKey: 'sk_test_example',
})
expect(response.status).toBe(200)
await expect(response.json()).resolves.toEqual({ data: { id: 'sub_123' }, status: 200 })
expect(logger.error).not.toHaveBeenCalled()
})

it('should preserve the forbidden status for an unauthenticated request', async () => {
const { logger, req } = createRequest()

const response = await stripeREST({ pluginConfig, req })

expect(response.status).toBe(403)
await expect(response.json()).resolves.toEqual({
message: 'Not allowed to perform this action.',
})
expect(mockStripeProxy).not.toHaveBeenCalled()
expect(logger.error).not.toHaveBeenCalled()
})

it('should log unexpected proxy errors and return a 500 response', async () => {
const proxyError = new Error('Stripe is unavailable')

mockStripeProxy.mockRejectedValue(proxyError)
const { logger, req } = createRequest({ user: { id: 'user_123' } })

const response = await stripeREST({ pluginConfig, req })

expect(response.status).toBe(500)
await expect(response.json()).resolves.toEqual({
message: 'An error has occurred in the Stripe plugin REST handler.',
})
expect(logger.error).toHaveBeenCalledWith({
err: proxyError,
msg: 'An error has occurred in the Stripe plugin REST handler.',
})
})
})
20 changes: 13 additions & 7 deletions packages/plugin-stripe/src/routes/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,19 @@ export const stripeREST = async (args: {
const { status } = responseJSON
responseStatus = status
} catch (error: unknown) {
const message = `An error has occurred in the Stripe plugin REST handler: '${JSON.stringify(
error,
)}'`
payload.logger.error(message)
responseStatus = 500
responseJSON = {
message,
if (error instanceof Forbidden) {
responseStatus = error.status
responseJSON = {
message: error.message,
}
} else {
const message = 'An error has occurred in the Stripe plugin REST handler.'

payload.logger.error({ err: error, msg: message })
responseStatus = 500
responseJSON = {
message,
}
}
}

Expand Down
83 changes: 83 additions & 0 deletions packages/plugin-stripe/src/utilities/stripeProxy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockStripeConstructor, mockStripeResources } = vi.hoisted(() => ({
mockStripeConstructor: vi.fn(),
mockStripeResources: {
subscriptions: {} as Record<string, unknown>,
},
}))

vi.mock('stripe', () => ({
default: class MockStripe {
subscriptions = mockStripeResources.subscriptions

constructor(secretKey: string, config: unknown) {
mockStripeConstructor(secretKey, config)
}
},
}))

import { stripeProxy } from './stripeProxy.js'

describe('stripeProxy', () => {
beforeEach(() => {
vi.clearAllMocks()
mockStripeResources.subscriptions = {}
})

it('should call a valid Stripe method with its resource context', async () => {
const mockList = vi.fn(function (this: { resource: string }, ...args: unknown[]) {
return Promise.resolve({ args, resource: this.resource })
})
const subscriptions = {
list: mockList,
resource: 'subscriptions',
}

mockStripeResources.subscriptions = subscriptions

const result = await stripeProxy({
stripeArgs: [{ limit: 2 }],
stripeMethod: 'subscriptions.list',
stripeSecretKey: 'sk_test_example',
})

expect(mockStripeConstructor).toHaveBeenCalledWith(
'sk_test_example',
expect.objectContaining({ apiVersion: '2022-08-01' }),
)
expect(mockList).toHaveBeenCalledWith({ limit: 2 })
expect(mockList.mock.contexts[0]).toBe(subscriptions)
expect(result).toEqual({
data: { args: [{ limit: 2 }], resource: 'subscriptions' },
status: 200,
})
})

it('should throw an explanatory error for an unknown Stripe method', async () => {
await expect(
stripeProxy({
stripeArgs: [],
stripeMethod: 'subscriptions.unknown',
stripeSecretKey: 'sk_test_example',
}),
).rejects.toThrow(
"The provided Stripe method of 'subscriptions.unknown' is not a part of the Stripe API.",
)
})

it('should reject non-array Stripe arguments', async () => {
const mockList = vi.fn()

mockStripeResources.subscriptions = { list: mockList }

await expect(
stripeProxy({
stripeArgs: { limit: 2 } as unknown as unknown[],
stripeMethod: 'subscriptions.list',
stripeSecretKey: 'sk_test_example',
}),
).rejects.toThrow("Argument 'stripeArgs' must be an array.")
expect(mockList).not.toHaveBeenCalled()
})
})
4 changes: 2 additions & 2 deletions packages/plugin-stripe/src/utilities/stripeProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export const stripeProxy: StripeProxy = async ({ stripeArgs, stripeMethod, strip
const contextToBind = stripe[topLevelMethod]
// NOTE: 'lodashGet' uses dot notation to get the property of an object
// NOTE: Stripe API methods using reference "this" within their functions, so we need to bind context
const foundMethod = lodashGet(stripe, stripeMethod).bind(contextToBind)
const foundMethod = lodashGet(stripe, stripeMethod)

if (typeof foundMethod === 'function') {
if (Array.isArray(stripeArgs)) {
try {
const stripeResponse = await foundMethod(...stripeArgs)
const stripeResponse = await foundMethod.bind(contextToBind)(...stripeArgs)
return {
data: stripeResponse,
status: 200,
Expand Down
Loading