Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/zendesk

src/zendesk.test.ts

177 lines · 6.8 KB · TypeScript
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
	extractListItems,
	extractPageInfo,
	mapArticle,
	mapMe,
	mapTicket,
	mapUser,
} from './models.ts'
import {
	API_TOKEN_SETUP_URL,
	API_USERNAME_SETUP_URL,
	DEFAULT_TOKEN_SECRET,
	DEFAULT_USERNAME_SECRET,
	OAUTH_CONNECT_URL,
	SUBDOMAIN_PLACEHOLDER,
	ZENDESK_CALLBACK_URL,
	apiTokenSetupUrl,
	apiUsernameSetupUrl,
	inferOperationFromPath,
	mutationPreview,
	normalizeSubdomain,
	normalizeZendeskPath,
	oauthConnectUrl,
	permissionForOperation,
	resolveIntegrationName,
	resolveSecretName,
	resolveUsernameSecretName,
	zendeskHost,
	zendeskUrl,
} from './setup.ts'

const here = dirname(fileURLToPath(import.meta.url))
const root = join(here, '..')

test('secret and integration names default and suffix accounts', () => {
	assert.equal(resolveIntegrationName(), 'zendesk')
	assert.equal(resolveSecretName(), DEFAULT_TOKEN_SECRET)
	assert.equal(resolveUsernameSecretName(), DEFAULT_USERNAME_SECRET)
	assert.equal(resolveIntegrationName({ account: 'default' }), 'zendesk')
	assert.equal(resolveIntegrationName({ account: 'work' }), 'zendesk-work')
	assert.equal(resolveSecretName({ account: 'work' }), 'zendeskApiToken-work')
	assert.equal(resolveUsernameSecretName({ account: 'work' }), 'zendeskApiUsername-work')
	assert.equal(resolveIntegrationName({ integrationName: 'zendesk-brand' }), 'zendesk-brand')
	assert.equal(resolveSecretName({ secretName: 'zendeskApiToken-brand' }), 'zendeskApiToken-brand')
})

test('subdomain is required input or stored config, never a baked-in account', () => {
	assert.equal(normalizeSubdomain('Acme'), 'acme')
	assert.equal(normalizeSubdomain('https://acme.zendesk.com/agent'), 'acme')
	assert.equal(zendeskHost('acme'), 'acme.zendesk.com')
	assert.throws(() => normalizeSubdomain('not a host'), /subdomain must be a Zendesk Support subdomain/)
	assert.doesNotMatch(OAUTH_CONNECT_URL, /kentcdodds/)
	assert.match(OAUTH_CONNECT_URL, /your-subdomain\.zendesk\.com/)
})

test('setup URLs are prefilled for OAuth and API token', () => {
	assert.match(OAUTH_CONNECT_URL, /provider=zendesk/)
	assert.match(OAUTH_CONNECT_URL, /oauth%2Fauthorizations%2Fnew/)
	assert.match(OAUTH_CONNECT_URL, /oauth%2Ftokens/)
	assert.match(OAUTH_CONNECT_URL, /allowedHosts=your-subdomain\.zendesk\.com/)
	assert.equal(ZENDESK_CALLBACK_URL, 'https://kody.codes/connect/oauth')
	assert.match(API_TOKEN_SETUP_URL, /name=zendeskApiToken/)
	assert.match(API_USERNAME_SETUP_URL, /name=zendeskApiUsername/)
	assert.match(API_TOKEN_SETUP_URL, /scope=user/)
	assert.match(oauthConnectUrl('zendesk-work', 'acme'), /provider=zendesk-work/)
	assert.match(oauthConnectUrl('zendesk-work', 'acme'), /acme\.zendesk\.com/)
	assert.match(apiTokenSetupUrl('zendeskApiToken-work', 'acme'), /name=zendeskApiToken-work/)
	assert.match(apiUsernameSetupUrl('zendeskApiUsername-work', 'acme', 'agent@example.com'), /agent%40example.com%2Ftoken/)
	assert.equal(SUBDOMAIN_PLACEHOLDER, 'your-subdomain')
})

test('mutationPreview dryRun skips confirm', () => {
	const preview = mutationPreview(
		{ dryRun: true },
		{
			method: 'POST',
			path: '/tickets',
			url: 'https://acme.zendesk.com/api/v2/tickets',
			body: { ticket: { subject: 'Reset password' } },
		},
	)
	assert.deepEqual(preview, {
		dryRun: true,
		method: 'POST',
		path: '/tickets',
		url: 'https://acme.zendesk.com/api/v2/tickets',
		body: { ticket: { subject: 'Reset password' } },
	})
})

test('mutationPreview requires confirm for live writes', () => {
	assert.throws(
		() =>
			mutationPreview(
				{},
				{
					method: 'PUT',
					path: '/tickets/1',
					url: 'https://acme.zendesk.com/api/v2/tickets/1',
					body: { ticket: { status: 'solved' } },
				},
			),
		/confirm: true/,
	)
	assert.equal(
		mutationPreview(
			{ confirm: true },
			{
				method: 'PUT',
				path: '/tickets/1',
				url: 'https://acme.zendesk.com/api/v2/tickets/1',
				body: { ticket: { status: 'solved' } },
			},
		),
		null,
	)
})

test('paths stay on the caller subdomain and infer operations', () => {
	assert.equal(normalizeZendeskPath('/tickets'), '/tickets')
	assert.match(
		zendeskUrl('/tickets', { per_page: 5 }, 'acme'),
		/^https:\/\/acme\.zendesk\.com\/api\/v2\/tickets\?per_page=5$/,
	)
	assert.throws(() => normalizeZendeskPath('https://example.com/tickets', 'acme'), /acme\.zendesk\.com/)
	assert.equal(inferOperationFromPath('GET', '/tickets'), 'tickets.read')
	assert.equal(inferOperationFromPath('GET', '/search'), 'tickets.read')
	assert.equal(inferOperationFromPath('POST', '/tickets'), 'tickets.write')
	assert.equal(inferOperationFromPath('GET', '/users/search'), 'users.read')
	assert.equal(inferOperationFromPath('POST', '/users'), 'users.write')
	assert.equal(inferOperationFromPath('GET', '/help_center/articles'), 'articles.read')
	assert.equal(inferOperationFromPath('POST', '/help_center/sections/1/articles'), 'articles.write')
	assert.equal(inferOperationFromPath('GET', '/users/me'), 'me.read')
	assert.equal(permissionForOperation('articles.write'), 'hc:write')
})

test('summaries stay generic and require ids from the payload', () => {
	assert.equal(mapTicket({ subject: 'Help' }), null)
	assert.equal(mapTicket({ id: 42, subject: 'Help' })?.subject, 'Help')
	assert.equal(mapUser({ name: 'Pat' }), null)
	assert.equal(mapUser({ id: 7, name: 'Pat', email: 'pat@example.com' })?.name, 'Pat')
	assert.equal(mapArticle({ title: 'Reset' }), null)
	assert.equal(mapArticle({ id: 9, title: 'Reset' })?.title, 'Reset')
	assert.equal(mapMe({ user: { id: 3, name: 'Agent', role: 'admin' } }).role, 'admin')
	assert.deepEqual(extractListItems({ tickets: [{ id: 1 }] }, ['tickets']), [{ id: 1 }])
	assert.equal(
		extractPageInfo({ next_page: 'https://acme.zendesk.com/api/v2/tickets?page=2', count: 40 }).hasNextPage,
		true,
	)
})

test('package is public MIT official zendesk with disabled job', () => {
	const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
	assert.equal(manifest.name, '@kody/zendesk')
	assert.equal(manifest.license, 'MIT')
	assert.equal(manifest.private, false)
	assert.equal(manifest.kody.id, 'zendesk')
	assert.equal(manifest.kody.jobs['account-health'].enabled, false)
	const readme = readFileSync(join(root, 'README.md'), 'utf8')
	assert.match(readme, /## Intent/)
	assert.match(readme, /https:\/\/kody\.codes\/@kody\/zendesk/)
	assert.match(readme, /\/account\/secrets\/new\?name=zendeskApiToken/)
	assert.match(readme, /\/account\/secrets\/new\?name=zendeskApiUsername/)
	assert.match(readme, /\/connect\/oauth\?provider=zendesk/)
	assert.doesNotMatch(readme, /\/community\/[0-9a-f-]{36}/)
	assert.doesNotMatch(readme, /hello@kentcdodds\.com/)
	assert.doesNotMatch(readme, /kentcdodds/)
	assert.doesNotMatch(readme, /betterWithKent/)
	const icon = readFileSync(join(root, 'community-icon.svg'), 'utf8')
	assert.match(icon, /Official Zendesk Z symbol/)
	assert.match(icon, /#03363D/i)
})