diff --git a/.gitignore b/.gitignore index 379644b..4efe13f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ coverage/ junit.xml package-lock.json .claude +.cursor/ diff --git a/bin/run b/bin/run.js similarity index 82% rename from bin/run rename to bin/run.js index d4ea366..1d416d6 100755 --- a/bin/run +++ b/bin/run.js @@ -11,8 +11,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const oclif = require('@oclif/core') +import { run, handle, flush } from '@oclif/core' -oclif.run() - .then(require('@oclif/core/flush')) - .catch(require('@oclif/core/handle')) +await run() + .then(flush) + .catch(handle) diff --git a/eslint.config.js b/eslint.config.js index e30b081..088dcf0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,12 +10,20 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const aioLibConfig = require('@adobe/eslint-config-aio-lib-config') -const pluginJest = require('eslint-plugin-jest') +import aioLibConfig from '@adobe/eslint-config-aio-lib-config' +import vitest from '@vitest/eslint-plugin' -module.exports = [ +export default [ ...aioLibConfig, - pluginJest.configs['flat/recommended'], + { + files: ['test/**'], + ...vitest.configs.recommended, + languageOptions: { + globals: { + ...vitest.environments.env.globals + } + } + }, { rules: { 'jsdoc/no-defaults': 'off' diff --git a/package.json b/package.json index 7b440a1..21bf42e 100644 --- a/package.json +++ b/package.json @@ -4,21 +4,23 @@ "description": "Generate and validate private certificates, and public key pairs for use with Adobe IO Console", "repository": "adobe/aio-cli-plugin-certificate", "homepage": "https://github.com/adobe/aio-cli-plugin-certificate", + "type": "module", "dependencies": { "@oclif/core": "^4.9.0", "debug": "^4.3.3", - "fs-extra": "^9.0.0", + "fs-extra": "^11.3.5", "node-forge": "^1.3.0" }, "devDependencies": { "@adobe/eslint-config-aio-lib-config": "^5.0.0", "eslint": "^9", - "eslint-plugin-jest": "^29", + "@vitest/coverage-v8": "^4.1.8", + "@vitest/eslint-plugin": "^1", "eslint-plugin-jsdoc": "^48", - "jest": "^29", "neostandard": "^0", "oclif": "^4.0.0", - "stdout-stderr": "^0.1.9" + "stdout-stderr": "^0.1.9", + "vitest": "^4.1.8" }, "engines": { "node": ">=20" @@ -43,22 +45,9 @@ "scripts": { "posttest": "eslint src test", "test": "npm run unit-tests", - "unit-tests": "jest --ci", + "unit-tests": "vitest run", "prepack": "oclif manifest && oclif readme --no-aliases", "postpack": "rm -f oclif.manifest.json", "version": "oclif readme && git add README.md" - }, - "jest": { - "collectCoverage": true, - "testPathIgnorePatterns": [ - "/tests/fixtures/" - ], - "coveragePathIgnorePatterns": [ - "/tests/fixtures/" - ], - "testEnvironment": "node", - "setupFilesAfterEnv": [ - "./test/jest.setup.js" - ] } } diff --git a/src/certificate.js b/src/certificate.js index fb476f3..a8de2f4 100644 --- a/src/certificate.js +++ b/src/certificate.js @@ -9,10 +9,10 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const debug = require('debug')('aio-cli-plugin-certificate:helpers') -const forge = require('node-forge') -const pki = forge.pki -const asn1 = forge.asn1 +import logDebug from 'debug' +import forge from 'node-forge' +const { pki, asn1 } = forge +const debug = logDebug('aio-cli-plugin-certificate:helpers') /** * Computes the SHA-1 digest of the entire DER-encoded x.509 certificate @@ -164,8 +164,4 @@ function verify (pemCert) { } } -module.exports = { - fingerprint, - generate, - verify -} +export { fingerprint, generate, verify } diff --git a/src/commands/certificate/fingerprint.js b/src/commands/certificate/fingerprint.js index af78d5b..09dd847 100644 --- a/src/commands/certificate/fingerprint.js +++ b/src/commands/certificate/fingerprint.js @@ -10,11 +10,12 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { Command, Args } = require('@oclif/core') -const fs = require('fs-extra') -const debug = require('debug')('aio-cli-plugin-certificate:fingerprint') +import { Command, Args } from '@oclif/core' +import fs from 'fs-extra' +import logDebug from 'debug' +import * as cert from '../../certificate.js' -const cert = require('../../certificate') +const debug = logDebug('aio-cli-plugin-certificate:fingerprint') class FingerprintCommand extends Command { async run () { @@ -47,4 +48,4 @@ FingerprintCommand.args = { }) } -module.exports = FingerprintCommand +export default FingerprintCommand diff --git a/src/commands/certificate/generate.js b/src/commands/certificate/generate.js index 295d18a..5f57eb4 100644 --- a/src/commands/certificate/generate.js +++ b/src/commands/certificate/generate.js @@ -10,11 +10,12 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { Command, Flags } = require('@oclif/core') -const fs = require('fs-extra') -const debug = require('debug')('aio-cli-plugin-certificate:generate') +import { Command, Flags } from '@oclif/core' +import fs from 'fs-extra' +import logDebug from 'debug' +import * as cert from '../../certificate.js' -const cert = require('../../certificate') +const debug = logDebug('aio-cli-plugin-certificate:generate') class GenerateCommand extends Command { async run () { @@ -79,4 +80,4 @@ GenerateCommand.flags = { }) } -module.exports = GenerateCommand +export default GenerateCommand diff --git a/src/commands/certificate/index.js b/src/commands/certificate/index.js index 2e4826c..1fe2518 100644 --- a/src/commands/certificate/index.js +++ b/src/commands/certificate/index.js @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { Command, Help } = require('@oclif/core') +import { Command, Help } from '@oclif/core' class AIOCommand extends Command { async run () { @@ -21,4 +21,4 @@ class AIOCommand extends Command { AIOCommand.description = 'Generate, fingerprint, or verify a certificate for use with Adobe I/O' -module.exports = AIOCommand +export default AIOCommand diff --git a/src/commands/certificate/verify.js b/src/commands/certificate/verify.js index baf0cca..524ab5d 100644 --- a/src/commands/certificate/verify.js +++ b/src/commands/certificate/verify.js @@ -10,11 +10,12 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { Command, Flags, Args } = require('@oclif/core') -const fs = require('fs-extra') -const debug = require('debug')('aio-cli-plugin-certificate:verify') +import { Command, Flags, Args } from '@oclif/core' +import fs from 'fs-extra' +import logDebug from 'debug' +import * as cert from '../../certificate.js' -const cert = require('../../certificate') +const debug = logDebug('aio-cli-plugin-certificate:verify') class VerifyCommand extends Command { async run () { @@ -77,4 +78,4 @@ VerifyCommand.args = { }) } -module.exports = VerifyCommand +export default VerifyCommand diff --git a/test/commands/certificate/fingerprint.test.js b/test/commands/certificate/fingerprint.test.js index 01762e3..65eb449 100644 --- a/test/commands/certificate/fingerprint.test.js +++ b/test/commands/certificate/fingerprint.test.js @@ -10,12 +10,12 @@ governing permissions and limitations under the License. */ // const { stdout } = require('stdout-stderr') -const commandPath = '../../../src/commands/certificate/fingerprint' -let TheCommand -jest.isolateModules(() => { - TheCommand = require(commandPath) -}) +import { vi } from 'vitest' +import mockFS from 'fs-extra' +// we don't import the command up here to allow for some tests to run with isolated mocks +// since vitest doesn't support isolateModules +const commandPath = '../../../src/commands/certificate/fingerprint.js' const validCertPem = ` -----BEGIN CERTIFICATE----- MIIDMTCCAhmgAwIBAgIHAWVCcDJVYDANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQD @@ -40,36 +40,60 @@ B9+DCYg= ` const validCertFingerprint = '38f65e26bd3869ec3ca029cc0b3df98de29172b9' -test('exports', async () => { - expect(typeof TheCommand).toEqual('function') -}) +describe('basic functionality', () => { + let TheCommand -test('description', async () => { - expect(TheCommand.description).toBeDefined() -}) + beforeAll(async () => { + vi.resetModules() + TheCommand = (await import(commandPath)).default + }) + + test('exports', async () => { + expect(typeof TheCommand).toEqual('function') + }) + + test('description', async () => { + expect(TheCommand.description).toBeDefined() + }) -test('args', async () => { - expect(Object.keys(TheCommand.args)[0]).toBeDefined() + test('args', async () => { + expect(Object.keys(TheCommand.args)[0]).toBeDefined() + }) }) -const mockConfig = { runHook: jest.fn().mockResolvedValue({ successes: [], failures: [] }) } +const mockConfig = { runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }) } describe('instance methods - mock forge', () => { - let CommandUnderTest, command, handleError, mockFS, mockForge - jest.isolateModules(() => { - CommandUnderTest = require(commandPath) - mockFS = require('fs-extra') - mockForge = require('node-forge') - jest.mock('node-forge') + let CommandUnderTest, command, handleError, mockForge + + beforeAll(async () => { + vi.resetModules() + // vitest does not support isolateModules, so we have to build our own sandbox mock here + vi.doMock('node-forge', async (importOriginal) => { + const mod = await importOriginal() + const forge = mod.default ?? mod + return { + ...mod, + default: { + ...forge, + pki: { + ...forge.pki, + certificateFromPem: vi.fn() + } + } + } + }) + CommandUnderTest = (await import(commandPath)).default + mockForge = (await import('node-forge')).default }) beforeEach(() => { command = new CommandUnderTest([], mockConfig) - handleError = jest.spyOn(command, 'error') + handleError = vi.spyOn(command, 'error') }) afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) test('run missing args', async () => { @@ -96,27 +120,28 @@ describe('instance methods - mock forge', () => { }) describe('instance methods - real forge', () => { - let CommandUnderTest, command, handleError, mockFS - jest.isolateModules(() => { - mockFS = require('fs-extra') - jest.unmock('node-forge') - CommandUnderTest = require(commandPath) + let CommandUnderTest, command, handleError + + beforeAll(async () => { + vi.resetModules() + vi.doUnmock('node-forge') + CommandUnderTest = (await import(commandPath)).default }) beforeEach(() => { command = new CommandUnderTest([], mockConfig) - handleError = jest.spyOn(command, 'error') + handleError = vi.spyOn(command, 'error') }) afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) test('run with valid cert pem', async () => { mockFS.existsSync.mockReturnValue(true) mockFS.readFileSync.mockReturnValue(Buffer.from(validCertPem)) command.argv = ['file'] - const logSpy = jest.spyOn(command, 'log') + const logSpy = vi.spyOn(command, 'log') await expect(command.run()).resolves.toBeUndefined() expect(logSpy).toHaveBeenCalledWith(validCertFingerprint) expect(handleError).not.toHaveBeenCalled() diff --git a/test/commands/certificate/generate.test.js b/test/commands/certificate/generate.test.js index 53d548b..1320907 100644 --- a/test/commands/certificate/generate.test.js +++ b/test/commands/certificate/generate.test.js @@ -10,10 +10,10 @@ governing permissions and limitations under the License. */ // const { stdout } = require('stdout-stderr') -const TheCommand = require('../../../src/commands/certificate/generate') - -const mockFS = require('fs-extra') -const forge = require('node-forge') +import { vi } from 'vitest' +import TheCommand from '../../../src/commands/certificate/generate.js' +import mockFS from 'fs-extra' +import forge from 'node-forge' test('exports', async () => { expect(typeof TheCommand).toEqual('function') @@ -23,18 +23,18 @@ test('description', async () => { expect(TheCommand.description).toBeDefined() }) -const mockConfig = { runHook: jest.fn().mockResolvedValue({ successes: [], failures: [] }) } +const mockConfig = { runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }) } describe('instance methods', () => { let command, handleError beforeEach(() => { command = new TheCommand([], mockConfig) - handleError = jest.spyOn(command, 'error') + handleError = vi.spyOn(command, 'error') }) afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) test('run -- no flags', async () => { diff --git a/test/commands/certificate/index.test.js b/test/commands/certificate/index.test.js new file mode 100644 index 0000000..7f522bf --- /dev/null +++ b/test/commands/certificate/index.test.js @@ -0,0 +1,37 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { vi } from 'vitest' +import TheCommand from '../../../src/commands/certificate/index.js' +import { Help } from '@oclif/core' + +describe('index', () => { + let showHelpSpy + + beforeEach(() => { + showHelpSpy = vi.spyOn(Help.prototype, 'showHelp').mockResolvedValue(undefined) + }) + + afterEach(() => { + showHelpSpy.mockRestore() + }) + + test('description', async () => { + expect(TheCommand.description).toEqual('Generate, fingerprint, or verify a certificate for use with Adobe I/O') + }) + + test('run Help command', async () => { + const command = new TheCommand([], { runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }) }) + await command.run() + expect(showHelpSpy).toHaveBeenCalledWith(['certificate', '--help']) + }) +}) diff --git a/test/commands/certificate/verify.test.js b/test/commands/certificate/verify.test.js index 81bb0d7..c69ff9a 100644 --- a/test/commands/certificate/verify.test.js +++ b/test/commands/certificate/verify.test.js @@ -9,12 +9,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -// const { stdout } = require('stdout-stderr') -const TheCommand = require('../../../src/commands/certificate/verify') +import { vi } from 'vitest' -const mockFS = require('fs-extra') -const mockForge = require('node-forge') -jest.mock('node-forge') +vi.mock('node-forge') + +import TheCommand from '../../../src/commands/certificate/verify.js' +import mockFS from 'fs-extra' +import mockForge from 'node-forge' const distantFuture = new Date() distantFuture.setFullYear(distantFuture.getFullYear() + 1) @@ -34,18 +35,18 @@ test('args', async () => { expect(Object.keys(TheCommand.args)[0]).toBeDefined() }) -const mockConfig = { runHook: jest.fn().mockResolvedValue({ successes: [], failures: [] }) } +const mockConfig = { runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }) } describe('instance methods', () => { let command, handleError beforeEach(() => { command = new TheCommand([], mockConfig) - handleError = jest.spyOn(command, 'error') + handleError = vi.spyOn(command, 'error') }) afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) test('run missing args', async () => { @@ -75,7 +76,7 @@ describe('instance methods', () => { test('run with invalid file ( false & true )', async () => { mockFS.existsSync.mockReturnValue(true) - mockForge.pki.certificateFromPem.mockReturnValue({ verify: jest.fn(() => true), validity: {} }) + mockForge.pki.certificateFromPem.mockReturnValue({ verify: vi.fn(() => true), validity: {} }) mockForge.pki.verifyCertificateChain.mockReturnValue(true) command.argv = ['file'] await expect(command.run()).resolves.toBe(true) @@ -84,7 +85,7 @@ describe('instance methods', () => { test('run with invalid file ( true & false )', async () => { mockFS.existsSync.mockReturnValue(true) - mockForge.pki.certificateFromPem.mockReturnValue({ verify: jest.fn(() => true), validity: {} }) + mockForge.pki.certificateFromPem.mockReturnValue({ verify: vi.fn(() => true), validity: {} }) mockForge.pki.verifyCertificateChain.mockReturnValue(false) command.argv = ['file'] await expect(command.run()).resolves.toBe(false) @@ -95,7 +96,7 @@ describe('instance methods', () => { mockFS.existsSync.mockReturnValue(true) mockForge.pki.certificateFromPem.mockReturnValue({ - verify: jest.fn(() => true), + verify: vi.fn(() => true), validity: { notAfter: now, notBefore: distantPast @@ -110,7 +111,7 @@ describe('instance methods', () => { mockFS.existsSync.mockReturnValue(true) mockForge.pki.certificateFromPem.mockReturnValue({ - verify: jest.fn(() => true), + verify: vi.fn(() => true), validity: { notAfter: distantFuture, notBefore: now @@ -125,7 +126,7 @@ describe('instance methods', () => { mockFS.existsSync.mockReturnValue(true) mockForge.pki.certificateFromPem.mockReturnValue({ - verify: jest.fn(() => true), + verify: vi.fn(() => true), validity: { notAfter: distantFuture, notBefore: now @@ -140,7 +141,7 @@ describe('instance methods', () => { mockFS.existsSync.mockReturnValue(true) mockForge.pki.certificateFromPem.mockReturnValue({ - verify: jest.fn(() => true), + verify: vi.fn(() => true), validity: { notAfter: now, notBefore: distantPast diff --git a/test/jest.setup.js b/test/jest.setup.js deleted file mode 100644 index 737b267..0000000 --- a/test/jest.setup.js +++ /dev/null @@ -1,9 +0,0 @@ -const { stdout } = require('stdout-stderr') - -beforeAll(() => stdout.start()) -afterAll(() => stdout.stop()) - -jest.setTimeout(30000) - -// dont touch the real fs -jest.mock('fs-extra') diff --git a/test/vitest.setup.js b/test/vitest.setup.js new file mode 100644 index 0000000..e01c3bd --- /dev/null +++ b/test/vitest.setup.js @@ -0,0 +1,20 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { stdout } from 'stdout-stderr' +import { beforeAll, afterAll, vi } from 'vitest' + +beforeAll(() => stdout.start()) +afterAll(() => stdout.stop()) + +// don't touch the real fs +vi.mock('fs-extra') diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..8796074 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,28 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + testTimeout: 30000, + setupFiles: ['./test/vitest.setup.js'], + include: ['test/**/*.test.js'], + exclude: ['**/node_modules/**'], + coverage: { + enabled: true, + include: ['src/**'] + } + } +})