Skip to content

Commit

Permalink
update mock cache
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostrider-05 committed Mar 7, 2025
1 parent ceed02b commit 243d7a4
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 23 deletions.
45 changes: 45 additions & 0 deletions docs/examples/mock/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PatreonMock, RouteBases, Routes } from 'patreon-api.ts'

const mock = new PatreonMock({
cache: {
initial: {
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: [],
goals: [],
tiers: [],
}
}],
]),
user: new Map([
['creator-id', {
item: {
about: null,
is_creator: true,
// ... All other user attributes
},
relationships: {
campaign: 'my-campaign-id',
memberships: [],
},
}]
])
}
}
})

export function getCachedCampaign () {
return mock.getMockHandlers().getCampaign.handler({
url: RouteBases.oauth2 + Routes.campaign('my-campaign-id'),
headers: mock.data.createHeaders(),
})
}
30 changes: 30 additions & 0 deletions docs/guide/features/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ To add relationships to the response, define the included items:

### Cache

The cache for mocking requests holds all attributes and relationships to act as a consistent server.

:::info 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:

<<< @/examples/mock/cache.ts{ts twoslash}

#### 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
Expand Down Expand Up @@ -94,4 +111,17 @@ Is there another popular testing / mocking framework that uses a completely diff

## 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`](../configuration#restretries) option) allows you to implement the same [retry system Patreon has](https://docs.patreon.com/#robust-retries).

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](#cache), the `paused`, `last_attempted_at` and `num_consecutive_times_failed` attributes will be updated to reflect the queue of the webhook.
2 changes: 1 addition & 1 deletion src/schemas/v2/mock/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class PatreonMockCache {
paused: false,
last_attempted_at: <never>null,
num_consecutive_times_failed: 0,
secret: '',
secret: '', // TODO: generate
},
relationships: {
campaign: body.data.relationships.campaign.data.id,
Expand Down
3 changes: 1 addition & 2 deletions src/schemas/v2/mock/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@ export class PatreonMock {
? options.data
: new PatreonMockData(options.data)

// @ts-expect-error TODO: fix this
this.webhooks = new PatreonMockWebhooks(options.webhooks ?? {}, this.data)
this.webhooks = new PatreonMockWebhooks(options.webhooks ?? {}, this.data, this.cache)
}

private validateHeaders (headers: Record<string, string> | Headers): void {
Expand Down
53 changes: 33 additions & 20 deletions src/schemas/v2/mock/webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { createHmac } from 'node:crypto'

import { PatreonWebhookTrigger, WebhookClient } from '../../../rest/v2/webhooks'
import { PatreonMockData, Webhook, WebhookPayload } from '../../../v2'
import { createBackoff, type RestRetriesOptions } from '../../../rest/v2/oauth2/rest'

import type { WebhookPayload } from '../../../payloads/v2/'
import type { Webhook } from '../resources/webhook'

import { type PatreonMockCache } from './cache'
import { type PatreonMockData } from './data'

interface PatreonMockWebhookHeaderData {
signature: string
event: PatreonWebhookTrigger
Expand Down Expand Up @@ -38,20 +43,22 @@ interface PatreonMockWebhookQueuedMessage {
last_attempted_at: string
}

interface PatreonMockWebhookStatus {
signature: string
success: boolean
errors: number
attempted_at: string
}

export class PatreonMockWebhooks {
public queuedMessages: Map<string, PatreonMockWebhookQueuedMessage> = new Map()

protected lastWebhookMessage: Map<string, {
signature: string
success: boolean
errors: number
attempted_at: string
}> = new Map()
protected lastWebhookMessage: Map<string, PatreonMockWebhookStatus> = new Map()

public constructor (
public options: PatreonMockWebhooksOptions,
protected data: PatreonMockData,
protected createTestPayload: <T extends PatreonWebhookTrigger>(trigger: T) => WebhookPayload<T>,
protected cache: PatreonMockCache,
) {}

public static getQueuedKey (webhookId: string, signature: string) {
Expand Down Expand Up @@ -122,12 +129,12 @@ export class PatreonMockWebhooks {

const timer = setTimeout(async () => {
const response = await fetch(webhook.uri, {
method: 'POST',
method: this.options.method ?? 'POST',
body,
headers,
})

this.lastWebhookMessage.set(webhook.id, {
this.setWebhookStatus(webhook.id, {
attempted_at: new Date().toISOString(),
signature,
success: response.ok,
Expand All @@ -148,11 +155,24 @@ export class PatreonMockWebhooks {
last_attempted_at: new Date().toISOString(),
})
}
}, retries === -1 ? 0 : createBackoff(this.options.retries.backoff)(retries))
}, retries === -1 ? 1 : createBackoff(this.options.retries.backoff)(retries))

return timer
}

private setWebhookStatus (id: string, status: PatreonMockWebhookStatus): void {
this.lastWebhookMessage.set(id, status)

const cached = this.cache.get('webhook', id)
if (cached != null) {
this.cache.store.edit('webhook', id, {
last_attempted_at: status.attempted_at,
num_consecutive_times_failed: status.errors,
paused: !status.success,
})
}
}

public sendQueuedMessage (
webhook: Pick<Webhook, 'secret' | 'uri'> & { id: string },
signature: string,
Expand All @@ -178,13 +198,6 @@ export class PatreonMockWebhooks {
return true
}

public async sendTestPayload <T extends PatreonWebhookTrigger>(
webhook: Pick<Webhook, 'secret' | 'uri'> & { id: string },
event: T
): Promise<number | null> {
return await this.send(webhook, event, this.createTestPayload(event))
}

public async send <T extends PatreonWebhookTrigger>(
webhook: Pick<Webhook, 'secret' | 'uri'> & { id: string },
event: T,
Expand All @@ -199,13 +212,13 @@ export class PatreonMockWebhooks {
})

const response = await fetch(webhook.uri, {
method: 'POST',
method: this.options.method ?? 'POST',
body,
headers,
})

if (response.ok) {
this.lastWebhookMessage.set(webhook.id, {
this.setWebhookStatus(webhook.id, {
attempted_at: new Date().toISOString(),
errors: 0,
signature,
Expand Down

0 comments on commit 243d7a4

Please sign in to comment.