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 {
API_KEY_SETUP_URL,
DEFAULT_API_KEY_SECRET,
DEFAULT_TOKEN_SECRET,
OAUTH_CONNECT_URL,
TOKEN_SETUP_URL,
TRELLO_CALLBACK_URL,
apiKeySetupUrl,
mutationPreview,
normalizeTrelloPath,
oauthConnectUrl,
resolveApiKeySecretName,
resolveIntegrationName,
resolveTokenSecretName,
tokenSetupUrl,
trelloAuthorizeUrl,
trelloUrl,
} from './setup.ts'
import { boardSummary, cardSummary, commentSummary, listSummary } from './models.ts'
const here = dirname(fileURLToPath(import.meta.url))
const root = join(here, '..')
test('secret and integration names default and suffix accounts', () => {
assert.equal(resolveIntegrationName(), 'trello')
assert.equal(resolveTokenSecretName(), DEFAULT_TOKEN_SECRET)
assert.equal(resolveApiKeySecretName(), DEFAULT_API_KEY_SECRET)
assert.equal(resolveIntegrationName({ account: 'default' }), 'trello')
assert.equal(resolveIntegrationName({ account: 'work' }), 'trello-work')
assert.equal(resolveTokenSecretName({ account: 'work' }), 'trelloToken-work')
assert.equal(resolveApiKeySecretName({ account: 'work' }), 'trelloApiKey-work')
assert.equal(resolveIntegrationName({ integrationName: 'trello-brand' }), 'trello-brand')
assert.equal(resolveTokenSecretName({ secretName: 'trelloToken-brand' }), 'trelloToken-brand')
assert.equal(
resolveApiKeySecretName({ apiKeySecretName: 'trelloApiKey-brand' }),
'trelloApiKey-brand',
)
})
test('setup URLs are prefilled for OAuth and secrets', () => {
assert.match(OAUTH_CONNECT_URL, /provider=trello/)
assert.match(OAUTH_CONNECT_URL, /apiBaseUrl=https%3A%2F%2Fapi\.trello\.com%2F1/)
assert.match(OAUTH_CONNECT_URL, /allowedHosts=api\.trello\.com/)
assert.equal(TRELLO_CALLBACK_URL, 'https://kody.codes/connect/oauth')
assert.match(API_KEY_SETUP_URL, /name=trelloApiKey/)
assert.match(API_KEY_SETUP_URL, /allowedHosts=api\.trello\.com/)
assert.match(API_KEY_SETUP_URL, /scope=user/)
assert.match(TOKEN_SETUP_URL, /name=trelloToken/)
assert.match(TOKEN_SETUP_URL, /allowedHosts=api\.trello\.com/)
assert.match(oauthConnectUrl('trello-work'), /provider=trello-work/)
assert.match(apiKeySetupUrl('trelloApiKey-work'), /name=trelloApiKey-work/)
assert.match(tokenSetupUrl('trelloToken-work'), /name=trelloToken-work/)
assert.match(trelloAuthorizeUrl(), /trello\.com\/1\/authorize/)
assert.match(trelloAuthorizeUrl(), /scope=read%2Cwrite/)
assert.match(trelloAuthorizeUrl(), /key=YOUR_API_KEY/)
})
test('mutationPreview dryRun skips confirm', () => {
const preview = mutationPreview(
{ dryRun: true },
{
method: 'POST',
path: '/cards',
body: { name: 'Write docs', idList: 'list-from-caller' },
},
)
assert.deepEqual(preview, {
dryRun: true,
method: 'POST',
path: '/cards',
body: { name: 'Write docs', idList: 'list-from-caller' },
})
})
test('mutationPreview requires confirm for live writes', () => {
assert.throws(
() =>
mutationPreview(
{},
{ method: 'POST', path: '/boards', body: { name: 'Planning' } },
),
/confirm: true/,
)
assert.equal(
mutationPreview(
{ confirm: true },
{ method: 'POST', path: '/boards', body: { name: 'Planning' } },
),
null,
)
})
test('trelloUrl stays on api.trello.com/1', () => {
assert.equal(normalizeTrelloPath('/members/me'), '/1/members/me')
assert.equal(normalizeTrelloPath('/1/boards'), '/1/boards')
assert.match(trelloUrl('/members/me'), /^https:\/\/api\.trello\.com\/1\/members\/me$/)
assert.match(trelloUrl('/boards', { filter: 'open' }), /filter=open/)
assert.throws(() => normalizeTrelloPath('https://example.com/1/members/me'), /api\.trello\.com/)
})
test('summaries stay generic and require ids from the payload', () => {
assert.equal(boardSummary({ name: 'Nope' }), null)
assert.deepEqual(boardSummary({ id: 'board-from-caller', name: 'Planning' })?.name, 'Planning')
assert.equal(listSummary({ name: 'Todo' }), null)
assert.equal(listSummary({ id: 'list-from-caller', name: 'Todo' })?.id, 'list-from-caller')
assert.equal(cardSummary({ name: 'Task' }), null)
assert.equal(cardSummary({ id: 'card-from-caller', name: 'Task' })?.name, 'Task')
assert.equal(commentSummary({ data: { text: 'Hi' } }), null)
})
test('package is public MIT official trello with disabled job', () => {
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
assert.equal(manifest.name, '@kody/trello')
assert.equal(manifest.license, 'MIT')
assert.equal(manifest.private, false)
assert.equal(manifest.kody.id, 'trello')
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\/trello/)
assert.match(readme, /\/account\/secrets\/new\?name=trelloApiKey/)
assert.match(readme, /\/account\/secrets\/new\?name=trelloToken/)
assert.match(readme, /\/connect\/oauth\?provider=trello/)
assert.doesNotMatch(readme, /\/community\/[0-9a-f-]{36}/)
assert.doesNotMatch(readme, /hello@kentcdodds\.com/)
assert.doesNotMatch(readme, /betterWithKent/)
assert.doesNotMatch(readme, /[0-9a-f]{24}/)
const icon = readFileSync(join(root, 'community-icon.svg'), 'utf8')
assert.match(icon, /#0052cc/i)
assert.match(icon, /Official Trello logomark/)
})