From 442c939beb9a47a2e2d799b1c7068b018691db29 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Apr 2020 18:20:14 +0100 Subject: [PATCH] feat(subscribers) (#9) * fix lint * copy-paste some tests * finish subscribers --- src/main.ts | 10 +- src/subscribers.ts | 105 ++++++++++++++ tests/subscribers.js | 325 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 435 insertions(+), 5 deletions(-) create mode 100644 tests/subscribers.js diff --git a/src/main.ts b/src/main.ts index 2b21dd9..69cd3cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,8 +5,8 @@ import * as emails from './emails'; import * as images from './images'; import * as newsletters from './newsletters'; import * as scheduledEmails from './scheduled-emails'; -// Import * as subscribers from './subscribers'; -// import * as tags from './tags'; +import * as subscribers from './subscribers'; +// Import * as tags from './tags'; // import * as unsubscribers from './unsubscribers'; export default { @@ -21,8 +21,8 @@ export default { images, newsletters, ping, - scheduledEmails - // Subscribers, - // tags, + scheduledEmails, + subscribers + // Tags, // unsubscribers }; diff --git a/src/subscribers.ts b/src/subscribers.ts index e69de29..1c576ce 100644 --- a/src/subscribers.ts +++ b/src/subscribers.ts @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */ +import client, {VERBS, RESOURCES} from './lib/client'; +import {validateNonEmptyObject, validatePresence} from './lib/validate'; + +interface SubscriberCreateFields { + readonly email: string; + readonly notes: string; + readonly referrer_url: string; + readonly tags: string[]; +} + +type SubscriberType = 'regular' | 'unactivated' | 'unpaid' | 'removed'; + +interface SubscriberQueryFilters { + readonly type?: SubscriberType; + readonly email?: string; + readonly tag?: string; +} + +interface SubscriberRecord extends SubscriberCreateFields { + readonly creation_date: string; + readonly id: string; + readonly metadata?: Record; + readonly secondary_id: number; + readonly subscriber_type: SubscriberType; + readonly source: string; + readonly tags: string[]; + readonly utm_campaign: string; + readonly utm_medium: string; + readonly utm_source: string; +} + +type SubscriberList = SubscriberRecord[]; + +const REQUIRED_FIELDS = ['email']; + +export async function list( + page = 1, + query: SubscriberQueryFilters = {} +): Promise { + return client.request(VERBS.GET, RESOURCES.SUBSCRIBERS, { + query: { + ...query, + page + } + }); +} + +export async function create(fields: SubscriberCreateFields): Promise { + validatePresence( + fields, + REQUIRED_FIELDS, + 'buttondown.subscribers.create() - email is required' + ); + return client.request(VERBS.POST, RESOURCES.SUBSCRIBERS, { + payload: fields + }); +} + +export async function get(id: string): Promise { + if (!id) { + throw new Error('buttondown.subscribers.get() - id is required'); + } + + return client.request(VERBS.GET, RESOURCES.SUBSCRIBERS, { + resourcePath: id + }); +} + +export async function put( + id: string, + fields: SubscriberRecord +): Promise { + if (!id) { + throw new Error('buttondown.subscribers.put() - id is required'); + } + + validatePresence( + fields, + REQUIRED_FIELDS, + 'buttondown.subscribers.put() - email is required' + ); + return client.request(VERBS.PUT, RESOURCES.SUBSCRIBERS, { + resourcePath: id, + payload: fields + }); +} + +export async function patch( + id: string, + fields: Partial +): Promise { + if (!id) { + throw new Error('buttondown.subscribers.patch() - id is required'); + } + + validateNonEmptyObject( + fields, + "buttondown.subscribers.patch() - can't patch subscriber to {}" + ); + return client.request(VERBS.PATCH, RESOURCES.SUBSCRIBERS, { + resourcePath: id, + payload: fields + }); +} diff --git a/tests/subscribers.js b/tests/subscribers.js new file mode 100644 index 0000000..d462f5a --- /dev/null +++ b/tests/subscribers.js @@ -0,0 +1,325 @@ +import test from 'ava'; +import nock from 'nock'; +import buttondown from '../dist/main'; +buttondown.setApiKey('super-secret-api-key'); + +nock.disableNetConnect(); + +const nockOptions = { + reqheaders: { + Authorization: 'Token super-secret-api-key' + } +}; + +const subscribersListPage1 = [ + { + creation_date: '2020-04', + id: 'some-id', + metadata: {}, + secondary_id: 1, + subscriber_type: 'regular', + source: 'string', + tags: ['tag1', 'tag2'], + utm_campaign: 'utm_campaign', + utm_medium: 'utm_campaign', + utm_source: 'utm_campaign' + } +]; + +test('subscribers.list() - no page - 200', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers') + .query({ + page: 1 + }) + .reply(200, subscribersListPage1); + + t.deepEqual(await buttondown.subscribers.list(), subscribersListPage1); +}); + +test('subscribers.list() - page 1 - 200', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers') + .query({ + page: 1 + }) + .reply(200, subscribersListPage1); + t.deepEqual(await buttondown.subscribers.list(1), subscribersListPage1); +}); + +test('subscribers.list() - page 2 - 200', async (t) => { + const subscribersListPage2 = [ + { + creation_date: '2020-04', + id: 'page-2-id', + metadata: {}, + secondary_id: 1, + subscriber_type: 'regular', + source: 'string', + tags: ['tag1', 'tag2'], + utm_campaign: 'utm_campaign', + utm_medium: 'utm_campaign', + utm_source: 'utm_campaign' + } + ]; + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers') + .query({ + page: 2 + }) + .reply(200, subscribersListPage2); + t.deepEqual(await buttondown.subscribers.list(2), subscribersListPage2); +}); + +test('subscribers.list() - query params filtering - 200', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers') + .query({ + page: 1, + email: 'justin@buttondown.email' + }) + .reply(200, subscribersListPage1); + t.deepEqual( + await buttondown.subscribers.list(1, {email: 'justin@buttondown.email'}), + subscribersListPage1 + ); +}); + +test('subscribers.list() - 401 - error', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers') + .query({ + page: 1 + }) + .reply(401, subscribersListPage1); + + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.list(); + }); + t.is(error.message, 'Response code 401 (Unauthorized)'); + t.is(error.url, 'https://api.buttondown.email/v1/subscribers'); + t.is(error.method, 'GET'); + t.is(error.payload, undefined); + t.deepEqual(error.query, {page: 1}); +}); + +const subscriberCreate = { + email: 'email' +}; + +test('subscribers.create() - 200', async (t) => { + const subscriberCreateResponse = { + creation_date: '2020-04', + id: 'some-id', + metadata: {}, + secondary_id: 1, + subscriber_type: 'regular', + source: 'string', + tags: ['tag1', 'tag2'], + utm_campaign: 'utm_campaign', + utm_medium: 'utm_campaign', + utm_source: 'utm_campaign' + }; + nock('https://api.buttondown.email', nockOptions) + .post('/v1/subscribers', subscriberCreate) + .reply(200, subscriberCreateResponse); + t.deepEqual( + await buttondown.subscribers.create(subscriberCreate), + subscriberCreateResponse + ); +}); + +test('subscribers.create() - missing email', async (t) => { + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.create({}); + }); + t.is(error.message, 'buttondown.subscribers.create() - email is required'); + t.is(error.url, undefined); + t.is(error.method, undefined); + t.is(error.payload, undefined); + t.is(error.query, undefined); +}); + +test('subscribers.create() - 400', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .post('/v1/subscribers', subscriberCreate) + .reply(400, {}); + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.create(subscriberCreate); + }); + t.is(error.message, 'Response code 400 (Bad Request)'); + t.is(error.url, 'https://api.buttondown.email/v1/subscribers'); + t.is(error.method, 'POST'); + t.is(error.payload, subscriberCreate); +}); + +test('subscribers.get() - 200', async (t) => { + const subscriberGetResponse = { + creation_date: '2020-04', + id: 'some-id', + metadata: {}, + secondary_id: 1, + subscriber_type: 'regular', + source: 'string', + tags: ['tag1', 'tag2'], + utm_campaign: 'utm_campaign', + utm_medium: 'utm_campaign', + utm_source: 'utm_campaign' + }; + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers/subscriber-id') + .reply(200, subscriberGetResponse); + t.deepEqual( + await buttondown.subscribers.get('subscriber-id'), + subscriberGetResponse + ); +}); + +test('subscribers.get() - missing id', async (t) => { + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.get(); + }); + + t.is(error.message, 'buttondown.subscribers.get() - id is required'); + t.is(error.url, undefined); + t.is(error.method, undefined); + t.is(error.payload, undefined); +}); + +test('subscribers.get() - 404', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .get('/v1/subscribers/subscriber-id') + .reply(404, {}); + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.get('subscriber-id'); + }); + t.is(error.message, 'Response code 404 (Not Found)'); + t.is(error.url, 'https://api.buttondown.email/v1/subscribers/subscriber-id'); + t.is(error.method, 'GET'); + t.is(error.payload, undefined); +}); + +const subscriberPut = { + ...subscriberCreate +}; + +test('subscribers.put() - 200', async (t) => { + const subscriberPutResponse = { + creation_date: '2020-04', + id: 'some-id', + metadata: {}, + secondary_id: 1, + subscriber_type: 'regular', + source: 'string', + tags: ['tag1', 'tag2'], + utm_campaign: 'utm_campaign', + utm_medium: 'utm_campaign', + utm_source: 'utm_campaign' + }; + nock('https://api.buttondown.email', nockOptions) + .put('/v1/subscribers/subscriber-id', subscriberPut) + .reply(200, subscriberPutResponse); + t.deepEqual( + await buttondown.subscribers.put('subscriber-id', subscriberPut), + subscriberPutResponse + ); +}); + +test('subscribers.put() - missing id', async (t) => { + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.put(); + }); + + t.is(error.message, 'buttondown.subscribers.put() - id is required'); + t.is(error.url, undefined); + t.is(error.method, undefined); + t.is(error.payload, undefined); +}); + +test('subscribers.put() - missing email', async (t) => { + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.put('subscriber-id', {}); + }); + t.is(error.message, 'buttondown.subscribers.put() - email is required'); + t.is(error.url, undefined); + t.is(error.method, undefined); + t.is(error.payload, undefined); + t.is(error.query, undefined); +}); + +test('subscribers.put() - 400', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .put('/v1/subscribers/subscriber-id', subscriberPut) + .reply(400, {}); + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.put('subscriber-id', subscriberPut); + }); + t.is(error.message, 'Response code 400 (Bad Request)'); + t.is(error.url, 'https://api.buttondown.email/v1/subscribers/subscriber-id'); + t.is(error.method, 'PUT'); + t.is(error.payload, subscriberPut); +}); + +const subscriberPatch = { + email: 'email' +}; + +test('subscribers.patch() - 200', async (t) => { + const subscriberPatchResponse = { + creation_date: '2020-04', + id: 'some-id', + metadata: {}, + secondary_id: 1, + subscriber_type: 'regular', + source: 'string', + tags: ['tag1', 'tag2'], + utm_campaign: 'utm_campaign', + utm_medium: 'utm_campaign', + utm_source: 'utm_campaign' + }; + nock('https://api.buttondown.email', nockOptions) + .patch('/v1/subscribers/subscriber-id', subscriberPatch) + .reply(200, subscriberPatchResponse); + t.deepEqual( + await buttondown.subscribers.patch('subscriber-id', subscriberPatch), + subscriberPatchResponse + ); +}); + +test('subscribers.patch() - missing id', async (t) => { + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.patch(); + }); + + t.is(error.message, 'buttondown.subscribers.patch() - id is required'); + t.is(error.url, undefined); + t.is(error.method, undefined); + t.is(error.payload, undefined); +}); + +test('subscribers.patch() - empty payload', async (t) => { + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.patch('subscriber-id', {}); + }); + t.is( + error.message, + "buttondown.subscribers.patch() - can't patch subscriber to {}" + ); + t.is(error.url, undefined); + t.is(error.method, undefined); + t.is(error.payload, undefined); + t.is(error.query, undefined); +}); + +test('subscribers.patch() - 400', async (t) => { + nock('https://api.buttondown.email', nockOptions) + .patch('/v1/subscribers/subscriber-id', subscriberPatch) + .reply(400, {}); + const error = await t.throwsAsync(async () => { + await buttondown.subscribers.patch('subscriber-id', subscriberPatch); + }); + t.is(error.message, 'Response code 400 (Bad Request)'); + t.is(error.url, 'https://api.buttondown.email/v1/subscribers/subscriber-id'); + t.is(error.method, 'PATCH'); + t.is(error.payload, subscriberPatch); +});