diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index b00d40411da..db3adc20652 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -30,7 +30,6 @@ import { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync' import { authWithApiKey as courierAuth, createCourierClient } from './courier' import { FeatureGates } from './feature-gates' import { VideoUriBuilder } from './views/util' -import axios from 'axios' import { RevenueCatClient } from './revenueCat' export * from './data-plane' diff --git a/packages/bsky/src/revenueCat.ts b/packages/bsky/src/revenueCat.ts index 55455f0075b..cb69cb8cf95 100644 --- a/packages/bsky/src/revenueCat.ts +++ b/packages/bsky/src/revenueCat.ts @@ -4,7 +4,7 @@ type Config = { webhookAuthorization: string | undefined } -type GetSubscriberResponse = { +export type GetSubscriberResponse = { subscriber: { entitlements: { [entitlementIdentifier: string]: unknown diff --git a/packages/bsky/tests/views/subscriptions.test.ts b/packages/bsky/tests/views/subscriptions.test.ts new file mode 100644 index 00000000000..9f2d91ac64d --- /dev/null +++ b/packages/bsky/tests/views/subscriptions.test.ts @@ -0,0 +1,136 @@ +import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' +import express from 'express' +import http from 'node:http' +import { once } from 'node:events' +import { GetSubscriberResponse } from '../../src/revenueCat' + +describe('subscriptions views', () => { + let network: TestNetwork + let sc: SeedClient + let revenueCatServer: http.Server + let revenueCatHandler: jest.Mock + let bskyUrl: string + + // account dids, for convenience + let alice: string + + beforeAll(async () => { + const revenueCatPort = 48567 + + revenueCatHandler = jest.fn() + revenueCatServer = await createMockRevenueCatService( + revenueCatPort, + revenueCatHandler, + ) + + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_subscriptions', + bsky: { + revenueCatV1ApiKey: 'any-key', + revenueCatV1Url: `http://localhost:${revenueCatPort}`, + }, + }) + bskyUrl = `http://localhost:${network.bsky.port}` + network.plc.getClient().updateData + sc = network.getSeedClient() + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice + }) + + afterAll(async () => { + await network.close() + revenueCatServer.close() + await once(revenueCatServer, 'close') + }) + + describe('webhook handler', () => { + it('sets the cache with the entitlements from the API response', async () => { + await network.bsky.db.db + .insertInto('subscription_entitlement') + .values({ + did: alice, + entitlements: JSON.stringify(['entitlement0']), + }) + .execute() + + const before = await network.bsky.db.db + .selectFrom('subscription_entitlement') + .selectAll() + .execute() + + expect(before).toStrictEqual([ + { did: alice, entitlements: ['entitlement0'] }, + ]) + + revenueCatHandler.mockImplementation((req, res) => { + const response: GetSubscriberResponse = { + subscriber: { + entitlements: { + entitlement1: {}, + entitlement2: {}, + }, + }, + } + res.json(response) + }) + + await fetch(`${bskyUrl}/webhooks/revenuecat`, { + method: 'POST', + body: JSON.stringify({ event: { app_user_id: alice } }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const after = await network.bsky.db.db + .selectFrom('subscription_entitlement') + .selectAll() + .execute() + + expect(after).toStrictEqual([ + { did: alice, entitlements: ['entitlement1', 'entitlement2'] }, + ]) + }) + + it('clears the cache if the API response returns no entitlements', async () => { + revenueCatHandler.mockImplementation((req, res) => { + const response: GetSubscriberResponse = { + subscriber: { + entitlements: {}, + }, + } + res.json(response) + }) + + await fetch(`${bskyUrl}/webhooks/revenuecat`, { + method: 'POST', + body: JSON.stringify({ event: { app_user_id: alice } }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const after = await network.bsky.db.db + .selectFrom('subscription_entitlement') + .selectAll() + .execute() + + expect(after).toHaveLength(0) + }) + }) +}) + +async function createMockRevenueCatService( + port: number, + handler: jest.Mock, +): Promise { + const app = express() + + app.use(express.json()) + app.get('/subscribers/:did', handler) + + const server = app.listen(port) + await once(server, 'listening') + return server +} diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index cf82a0220d6..721ec7acc87 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -68,9 +68,13 @@ export class TestBsky { ...cfg, adminPasswords: [ADMIN_PASSWORD], revenueCatV1Url: - process.env.BSKY_REVENUE_CAT_V1_URL || 'https://api.revenuecat.com/v1', - revenueCatV1ApiKey: process.env.BSKY_REVENUE_CAT_V1_API_KEY, + cfg.revenueCatV1Url || + process.env.BSKY_REVENUE_CAT_V1_URL || + 'https://api.revenuecat.com/v1', + revenueCatV1ApiKey: + cfg.revenueCatV1ApiKey || process.env.BSKY_REVENUE_CAT_V1_API_KEY, revenueCatWebhookAuthorization: + cfg.revenueCatWebhookAuthorization || process.env.BSKY_REVENUE_CAT_WEBHOOK_AUTHORIZATION, })