Skip to content

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

Package listing

@kentcdodds/github

src/actions/put-secret.node.test.ts

122 lines · 2.9 KB · TypeScript
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import nacl from 'tweetnacl'
import sealedBox from 'tweetnacl-sealedbox-js'

import putActionsSecret from './put-secret.ts'
import {
	decodeBase64,
	encodeBase64,
	sealActionsSecret,
} from './sealed-box.ts'

function utf8(bytes: Uint8Array) {
	return new TextDecoder().decode(bytes)
}

describe('sealActionsSecret', () => {
	it('round-trips through a tweetnacl sealed box', () => {
		const keyPair = nacl.box.keyPair()
		const publicKey = encodeBase64(keyPair.publicKey)
		const sealed = sealActionsSecret('cache-token-value', publicKey)
		const opened = sealedBox.open(
			decodeBase64(sealed),
			keyPair.publicKey,
			keyPair.secretKey,
		)
		assert.equal(utf8(opened), 'cache-token-value')
	})
})

describe('putActionsSecret', () => {
	it('encrypts, PUTs, and never returns the plaintext', async () => {
		const keyPair = nacl.box.keyPair()
		const calls: Array<{
			path: string
			method?: string
			body?: unknown
		}> = []

		const result = await putActionsSecret(
			{
				owner: 'kentcdodds',
				repo: 'kody',
				name: 'nx_self_hosted_remote_cache_access_token',
				value: 'super-secret-token',
			},
			async (options) => {
				calls.push({
					path: options.path,
					method: options.method,
					body: options.body,
				})
				if (options.path.endsWith('/public-key')) {
					return {
						account: 'bot',
						url: options.path,
						ok: true,
						status: 200,
						statusText: 'OK',
						headers: {},
						data: {
							key_id: 'key-123',
							key: encodeBase64(keyPair.publicKey),
						},
						text: '',
					}
				}
				return {
					account: 'bot',
					url: options.path,
					ok: true,
					status: 201,
					statusText: 'Created',
					headers: {},
					data: null,
					text: '',
				}
			},
		)

		assert.deepEqual(result, {
			owner: 'kentcdodds',
			repo: 'kody',
			name: 'NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN',
			environment: null,
			created: true,
			updated: false,
			status: 201,
			keyId: 'key-123',
			generated: false,
		})
		assert.equal(
			JSON.stringify(result).includes('super-secret-token'),
			false,
		)
		assert.equal(calls[0]?.path, '/repos/kentcdodds/kody/actions/secrets/public-key')
		assert.equal(
			calls[1]?.path,
			'/repos/kentcdodds/kody/actions/secrets/NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN',
		)
		assert.equal(calls[1]?.method, 'PUT')
		const body = calls[1]?.body as {
			encrypted_value: string
			key_id: string
		}
		assert.equal(body.key_id, 'key-123')
		assert.equal(body.encrypted_value.includes('super-secret-token'), false)
		const opened = sealedBox.open(
			decodeBase64(body.encrypted_value),
			keyPair.publicKey,
			keyPair.secretKey,
		)
		assert.equal(utf8(opened), 'super-secret-token')
	})

	it('rejects missing value and generateBytes together', async () => {
		await assert.rejects(
			() => putActionsSecret({ owner: 'kentcdodds', repo: 'kody', name: 'FOO' }),
			/Provide value or generateBytes/,
		)
	})
})