Testing & sandbox
Since the Patreon API is readonly, you can mock responses to test if your application works.
Testing & Mocking
Random vs cache
All methods that return mocked data implement the following flow:
- by default: if an item is found in the cache, it returns that item. Otherwise it will create a random item of the same resource
- with
options.cache
equal tofalse
: returns a random item of the resource - with
options.random
equal tofalse
: if an item is found in the cache, it returns that item. Otherwise returns an empty string. - with
options.random
equal tofalse
andoptions.cache
equal tofalse
: returns an empty string
Validation
You can also enable validation to ensure your mocked requests are valid. If validation fails, it will throw an error (does not change the response). The following items can be validated:
- request path (always validated): whether the url of the request is a valid API url
headers
: specify the headers (and values) that must be present on the requestquery
: whether the query of the url will be validated, such that every relationship is valid for the url and each attribute exists
import { PatreonMock } from 'patreon-api.ts'
const mock = new PatreonMock({
validation: {
query: true,
headers: [
'User-Agent',
'Content-Type',
'Authorization',
],
},
})
try {
mock.getMockAgentReplyCallback()
} catch (err) {
console.log('Invalid request: ' + err)
}
Random data
You can create a random:
- id:
PatreonMockData.createId(type: Type)
- resource:
PatreonMockData.random[Type](id: string)
External library
The random data generator in this library is quite simple at the moment, so I suggest to use a random generator library like faker-js
.
Also note that each value is generated independent from other attributes.
With an id and (partial) resource, you can create an API response. The response attributes (and relationships) will be reflected from the query.
import { PatreonMockData, Type } from 'patreon-api.ts'
const data = new PatreonMockData({
// Overwrite attributes to be fixed values
// Or specify your own random methods
resources: {
campaign() {
return {
is_monthly: true,
}
},
}
})
const randomId = data.createId(Type.Campaign)
const randomCampaign = data.random.campaign(randomId)
export const randomCampaignPayload = data.getSingleResponsePayload(
Type.Campaign,
{
attributes: { campaign: ['patron_count'] },
includes: [],
}, {
id: randomId,
item: randomCampaign,
relatedItems: [],
}
)
To add relationships to the response, define the included items:
import { PatreonMockData, Type } from 'patreon-api.ts'
const data = new PatreonMockData()
const creatorId = data.createId(Type.User)
const campaignId = data.createId(Type.Campaign)
export const randomCampaignWithCreatorPayload = data.getSingleResponsePayload(
Type.Campaign,
{
attributes: { campaign: ['patron_count'] },
includes: ['creator'],
}, {
id: data.createId(Type.Campaign),
item: data.random.campaign(campaignId),
relatedItems: data.createRelatedItems(Type.Campaign, {
items: [
{
attributes: data.random.user(creatorId),
id: creatorId,
type: Type.User,
}
]
}),
}
)
Cache
The cache for mocking requests holds all attributes and relationships to act as a consistent server.
Relationships
The cache will only require an id for relationships and rebuild a response using the relation map for that resource. When a resource is not in the cache, the onMissingRelationship
option will decide what action is taken: throw an error, log a warning or nothing (default).
To define items in the cache, use the intitial
option:
import { PatreonMock, RouteBases, Routes } from 'patreon-api.ts'
const mock = new PatreonMock({
cache: {
initial: {
// @ts-expect-error Remove this when all attributes are added
campaign: new Map([
['my-campaign-id', {
item: {
patron_count: 7,
is_monthly: true,
// ... All other campaign attributes
},
relationships: {
creator: 'creator-id',
// If the campaign has benefits, goals or tiers
// Add these also the cache
benefits: [],
// If no relationship is configured for this item
// Use null instead of an array / empty string
goals: null,
tiers: [],
}
}],
]),
// @ts-expect-error Remove this when all attributes are added
user: new Map([
['creator-id', {
item: {
about: null,
is_creator: true,
// ... All other user attributes
},
relationships: {
campaign: 'my-campaign-id',
memberships: [],
},
}]
])
}
}
})
export function getCachedCampaignResponse () {
return mock.getMockHandlers().getCampaign.handler({
url: RouteBases.oauth2 + Routes.campaign('my-campaign-id'),
headers: mock.data.createHeaders(),
})
}
Write requests
For POST
, PATCH
and DELETE
the cache will update the cache using setRequestBody
, .e.g delete an item (will not delete or change relationships), update attributes or add a new item. This is done by the exposed handlers or callbacks and the only supported resource is 'webhook'
.
Frameworks
OpenAPI
See the API reference for an OpenAPI schema for the Patreon V2 API. This is an unofficial schema and based on the official documentation, made with this library.
MockAgent
To intercept a request to the Patreon API you can use undici's MockAgent
API to mock API responses.
import { PatreonMock, Routes } from 'patreon-api.ts'
declare const agent: import('undici-types').MockAgent
const mockAPI = new PatreonMock()
// Intercept any API call
agent
.get(PatreonMock.origin)
.intercept({ path: PatreonMock.pathFilter })
.reply(mockAPI.getMockAgentReplyCallback())
.delay(600)
// Intercept a specific API route
agent
.get(PatreonMock.origin)
.intercept({ path: PatreonMock.path + Routes.identity() })
.reply(mockAPI.getMockAgentReplyCallback())
.delay(600)
import { PatreonMock, PatreonMockData, Routes, Type } from 'patreon-api.ts'
declare const mockAgent: import('undici-types').MockAgent
const data = new PatreonMockData()
const testUserPayload = data.getSingleResponsePayload(Type.User, {
includes: [],
attributes: { user: ['full_name'] },
}, {
id: data.createId(Type.User),
item: { full_name: 'My test username' },
relatedItems: [],
})
mockAgent
.get(PatreonMock.origin)
.intercept({ path: PatreonMock.path + Routes.identity() })
.reply(
200,
JSON.stringify(testUserPayload),
{ headers: { 'Content-Type': 'application/json'} },
)
MSW
You can also intercept requests by using request handlers for some or all routes. A popular API using handlers is Mock Service Worker.
transformResponse
By default a handler will return an object with a status, body and headers. You can use this option to transform this object into a Response
or something else for every handler.
import { http } from 'msw'
import { PatreonMock } from 'patreon-api.ts'
const mockAPI = new PatreonMock()
const mockHandlers = mockAPI.getMockHandlers({
transformResponse(response) {
return new Response(response.body, {
headers: response.headers,
status: response.status,
})
},
})
export const handlers = [
http.get(mockHandlers.getCampaign.url, ({ request }) => {
return mockHandlers.getCampaign.handler({
headers: request.headers,
url: request.url,
})
}),
http.post(mockHandlers.createWebhook.url, async ({ request }) => {
return mockHandlers.createWebhook.handler({
headers: request.headers,
url: request.url,
body: await request.text(),
})
}),
]
import { http } from 'msw'
import { PatreonMock } from 'patreon-api.ts'
const mockAPI = new PatreonMock()
const mockHandlers = mockAPI.getMockHandlers({
transformResponse(response) {
return new Response(response.body, {
headers: response.headers,
status: response.status,
})
},
})
export const allHandlers = Object.values(mockHandlers).map(handler => {
return http[handler.method](handler.url, async ({ request }) => {
return handler.handler({
headers: request.headers,
url: request.url,
body: handler.method !== 'get'
? await request.text()
: null,
})
})
})
import { http } from 'msw'
import { PatreonMock } from 'patreon-api.ts'
const mockAPI = new PatreonMock()
const mockHandlers = mockAPI.getMockHandlers()
export const errorHandlers = [
http.get(mockHandlers.getCampaign.url, () => {
return new Response(JSON.stringify({ errors: [
mockAPI.data.createError(400),
]}), {
status: 400,
headers: {},
})
})
]
Other
Is there another popular testing / mocking framework that uses a completely different API? Open an issue on GitHub to add new mocking functionality and/or guides.
Webhooks
You can use webhook mocking to test your implementation of your server. You can create a request with createRequest
or send it with send
to an external server.
Retries
The retries
option (this uses the same implementation as client rest.retries
option) allows you to implement the same retry system Patreon has.
The send
method has a return type of Promise<number | null>
. If the type is a number
, the server has returned a succesful response. Otherwise (with retries
enabled), it will return null
and add / update the message to the queuedMessages
. With no retry options, it will return the status of the failed request. When a message is retried succesfully:
- the message will be deleted from the queue
- all other messages from the same webhook will be retried immediately
The sendQueuedMessage
/ sendQueuedMessages
methods will trigger a retry manually for one / all queued messages.
When a webhook is stored in the mock cache, the paused
, last_attempted_at
and num_consecutive_times_failed
attributes will be updated to reflect the queue of the webhook.