diff --git a/packages/app/macros/__tests__/macros.test.ts b/packages/app/macros/__tests__/macros.test.ts index 589a0004c..87f147871 100644 --- a/packages/app/macros/__tests__/macros.test.ts +++ b/packages/app/macros/__tests__/macros.test.ts @@ -4,9 +4,11 @@ import { getModel } from '../../models/service' import GLDAS_CLM10SUBP_3H_001 from '../../models/__tests__/__fixtures__/collection-metadata/GLDAS_CLM10SUBP_3H_001.json' import OML1BRVG_003 from '../../models/__tests__/__fixtures__/collection-metadata/OML1BRVG_003.json' import collectionMetadataModel from '../../models/__tests__/__fixtures__/models/collection-metadata.json' +import { getAllWebhookURLs } from '../service' import faqsModelWithOneMacro from './__fixtures__/faqs-model-one-macro.json' describe('Template Macros', () => { + const env = process.env let db beforeAll(async () => { @@ -23,6 +25,14 @@ describe('Template Macros', () => { await db.collection('Collection Metadata').deleteMany({}) }) + beforeEach(() => { + process.env = { ...env } + }) + + afterEach(() => { + process.env = { ...env } + }) + describe('list', () => { it('gets values from the path to populate the schema', async () => { const [error, model] = await getModel('FAQs', { @@ -75,4 +85,70 @@ describe('Template Macros', () => { `) }) }) + + describe('webhooks', () => { + test('gets webhook URLs as ErrorData', async () => { + // NOTE: this line needs to be left as escaped JSON. + // prettier-ignore + process.env.UI_WEBHOOKS = +"[{\"token\":\"an-example-token-for-endpoint-1\",\"URL\":\"http://example.com/1\"}]" + + const [error, webhookURLs] = await getAllWebhookURLs() + + expect(error).toBeNull() + expect(webhookURLs).toMatchInlineSnapshot(` + Array [ + "http://example.com/1", + ] + `) + }) + + test('requires escaped JSON only because the env parser does', async () => { + process.env.UI_WEBHOOKS = + '[{"token":"an-example-token-for-endpoint-1","URL":"http://example.com/1"}]' + + const [stringError, stringWebhookURLs] = await getAllWebhookURLs() + + expect(stringError).toBeNull() + expect(stringWebhookURLs).toMatchInlineSnapshot(` + Array [ + "http://example.com/1", + ] + `) + + // @ts-ignore + process.env.UI_WEBHOOKS = [ + { + token: 'an-example-token-for-endpoint-1', + URL: 'http://example.com/1', + }, + ] + + const [literalError, literalWebhookURLs] = await getAllWebhookURLs() + + expect(literalError).toBeNull() + expect(literalWebhookURLs).toMatchInlineSnapshot(` + Array [ + "http://example.com/1", + ] + `) + }) + + test('handles unset and nullish env values', async () => { + process.env.UI_WEBHOOKS = null + + const [nullError, nullWebhookURLs] = await getAllWebhookURLs() + + expect(nullError).toBeNull() + expect(nullWebhookURLs).toMatchInlineSnapshot(`Array []`) + + // @ts-ignore + delete process.env.UI_WEBHOOKS + + const [unsetError, unsetWebhookURLs] = await getAllWebhookURLs() + + expect(unsetError).toBeNull() + expect(unsetWebhookURLs).toMatchInlineSnapshot(`Array []`) + }) + }) }) diff --git a/packages/app/utils/__tests__/api.test.ts b/packages/app/utils/__tests__/api.test.ts new file mode 100644 index 000000000..1f6e1eec4 --- /dev/null +++ b/packages/app/utils/__tests__/api.test.ts @@ -0,0 +1,37 @@ +import { parseResponse } from '../api' + +describe('parseResponse', () => { + const responseMock = { + headers: null, + async json() { + return { foo: 'bar' } + }, + async text() { + return 'foo is bar' + }, + } as unknown as Response + + beforeAll(() => { + // @ts-ignore + responseMock.headers = new Map() + }) + + test('reads Content-Type header to determine parse type', async () => { + responseMock.headers.set('content-type', 'text/plain; charset=utf-8') + const resultText = await parseResponse(responseMock) + expect(resultText).toEqual(await responseMock.text()) + + responseMock.headers.set('content-type', 'application/json; charset=utf-8') + const resultJSON = await parseResponse(responseMock) + expect(resultJSON).toEqual(await responseMock.json()) + }) + + test('defaults to text()', async () => { + responseMock.headers.set( + 'content-type', + 'multipart/form-data; boundary=ExampleBoundaryString' + ) + const resultText = await parseResponse(responseMock) + expect(resultText).toEqual(await responseMock.text()) + }) +}) diff --git a/packages/app/webhooks/__tests__/webhooks.test.ts b/packages/app/webhooks/__tests__/webhooks.test.ts new file mode 100644 index 000000000..15c96aba9 --- /dev/null +++ b/packages/app/webhooks/__tests__/webhooks.test.ts @@ -0,0 +1,95 @@ +import type { ModelWithWorkflow } from '../../models/types' +import { getAllWebhookConfigs, getWebhookConfig, invokeWebhook } from '../service' + +describe('Webhook Service', () => { + const env = process.env + + beforeAll(() => { + global.fetch = jest.fn(() => + Promise.resolve({ + headers: { + get() { + return 'text/plain; charset=utf-8' + }, + }, + ok: true, + json: () => Promise.resolve({ foo: 'bar' }), + text: () => Promise.resolve('foo is bar'), + }) + ) as jest.Mock + }) + + beforeEach(() => { + process.env = { ...env } + }) + + afterEach(() => { + process.env = { ...env } + }) + + describe('getAllWebhookConfigs', () => { + test('gets all webhook configs as ErrorData', () => { + // NOTE: this line needs to be left as escaped JSON. + // prettier-ignore + process.env.UI_WEBHOOKS = +"[{\"token\":\"an-example-token-for-endpoint-1\",\"URL\":\"http://example.com/1\"}, {\"token\":\"an-example-token-for-endpoint-2\",\"URL\":\"http://example.com/2\"}]" + + const [error, data] = getAllWebhookConfigs() + + expect(error).toBeNull() + expect(data).toMatchInlineSnapshot(` + Array [ + Object { + "URL": "http://example.com/1", + "token": "an-example-token-for-endpoint-1", + }, + Object { + "URL": "http://example.com/2", + "token": "an-example-token-for-endpoint-2", + }, + ] + `) + }) + }) + + describe('getWebhookConfig', () => { + test('gets webhook config, matching by URL', () => { + // NOTE: this line needs to be left as escaped JSON. + // prettier-ignore + process.env.UI_WEBHOOKS = +"[{\"token\":\"an-example-token-for-endpoint-1\",\"URL\":\"http://example.com/1\"}, {\"token\":\"an-example-token-for-endpoint-2\",\"URL\":\"http://example.com/2\"}]" + + const [error, data] = getWebhookConfig('http://example.com/2') + + expect(error).toBeNull() + expect(data).toMatchInlineSnapshot(` + Object { + "URL": "http://example.com/2", + "token": "an-example-token-for-endpoint-2", + } + `) + }) + }) + + describe('invokeWebhook', () => { + test('invokes (fetches) a webhook URL', async () => { + const fetchSpy = jest.spyOn(global, 'fetch') + // NOTE: this line needs to be left as escaped JSON. + // prettier-ignore + process.env.UI_WEBHOOKS = +"[{\"token\":\"an-example-token-for-endpoint-1\",\"URL\":\"http://example.com/1\"}, {\"token\":\"an-example-token-for-endpoint-2\",\"URL\":\"http://example.com/2\"}]" + + const [configError, webhook] = getWebhookConfig('http://example.com/2') + const [error, result] = await invokeWebhook(webhook, { + model: {} as ModelWithWorkflow, + document: {}, + state: 'Draft', + }) + + expect(configError).toBeNull() + expect(error).toBeNull() + expect(result).toMatchInlineSnapshot(`"foo is bar"`) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + }) +})