diff --git a/.gitignore b/.gitignore index 2d8d77e..64368b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage/ node_modules/ package-lock.json +.kiro diff --git a/changelog.md b/changelog.md index 1216095..5cb5149 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,13 @@ --- +## [6.0.0 - 6.0.1] 2025-11-28 + +- node >= 22 +- native test runner + +--- + ## [5.0.0] 2024-09-24 - Updated deps and node > 20 diff --git a/package.json b/package.json index 0840562..efad212 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/env", - "version": "6.0.0", + "version": "6.0.1", "description": "Manage your Architect app's environment variables", "main": "src/index.js", "bin": { @@ -9,8 +9,8 @@ "scripts": { "test": "npm run lint && npm run coverage", "test:nolint": "npm run coverage", - "test:unit": "cross-env tape 'test/**/*-tests.js'", - "coverage": "nyc --reporter=lcov --reporter=text npm run test:unit", + "test:unit": "node --test 'test/**/*-tests.js'", + "coverage": "node --test --experimental-test-coverage 'test/**/*-tests.js'", "lint": "eslint . --fix", "rc": "npm version prerelease --preid RC" }, @@ -33,19 +33,13 @@ "dependencies": { "@architect/inventory": "~6.0.0", "@architect/parser": "~8.0.1", - "@architect/utils": "~6.0.0", + "@architect/utils": "~6.0.1", "@aws-lite/client": "^0.23.2", "@aws-lite/ssm": "^0.2.3", "dotenv": "~17.2.3" }, "devDependencies": { "@architect/eslint-config": "~3.0.0", - "cross-env": "~10.1.0", - "eslint": "~9.39.1", - "nyc": "~17.1.0", - "proxyquire": "~2.1.3", - "sinon": "~21.0.0", - "tap-arc": "^1.2.2", - "tape": "^5.7.5" + "eslint": "~9.39.1" } } diff --git a/test/_add-remove-tests.js b/test/_add-remove-tests.js index e7ac920..08fe395 100644 --- a/test/_add-remove-tests.js +++ b/test/_add-remove-tests.js @@ -1,5 +1,5 @@ -/* let test = require('tape') -let sinon = require('sinon') +/* const { test, mock } = require('node:test') +const assert = require('node:assert') let AWS = require('aws-sdk') let aws = require('aws-sdk-mock') aws.setSDKInstance(AWS) @@ -12,8 +12,7 @@ let params = { inventory: { inv: { aws: { region: 'us-west-2' }, } }, update } -test('Add should error on invalid environment', t => { - t.plan(1) +test('Add should error on invalid environment', () => { let item = { action: 'add', env: 'idk', @@ -21,13 +20,12 @@ test('Add should error on invalid environment', t => { value: 'bar', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.match(err.message, /Invalid environment/, 'Errored on invalid environment') - else t.fail('Expected invalid environment error') + if (err) assert.match(err.message, /Invalid environment/, 'Errored on invalid environment') + else assert.fail('Expected invalid environment error') }) }) -test('Remove should error on invalid environment', t => { - t.plan(1) +test('Remove should error on invalid environment', () => { let item = { action: 'remove', env: 'idk', @@ -35,13 +33,12 @@ test('Remove should error on invalid environment', t => { value: 'bar', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.match(err.message, /Invalid environment/, 'Errored on invalid environment') - else t.fail('Expected invalid environment error') + if (err) assert.match(err.message, /Invalid environment/, 'Errored on invalid environment') + else assert.fail('Expected invalid environment error') }) }) -test('Adding should callback with error on invalid name', t => { - t.plan(1) +test('Adding should callback with error on invalid name', () => { let item = { action: 'add', env: 'testing', @@ -49,13 +46,12 @@ test('Adding should callback with error on invalid name', t => { value: 'bar', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.match(err.message, /Invalid name/, 'Errored on invalid name') - else t.fail('Expected invalid name error') + if (err) assert.match(err.message, /Invalid name/, 'Errored on invalid name') + else assert.fail('Expected invalid name error') }) }) -test('Remove should callback with error on invalid name', t => { - t.plan(1) +test('Remove should callback with error on invalid name', () => { let item = { action: 'remove', env: 'testing', @@ -63,26 +59,25 @@ test('Remove should callback with error on invalid name', t => { value: 'bar', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.match(err.message, /Invalid name/, 'Errored on invalid name') - else t.fail('Expected invalid name error') + if (err) assert.match(err.message, /Invalid name/, 'Errored on invalid name') + else assert.fail('Expected invalid name error') }) }) -test('Add should error on missing value', t => { - t.plan(1) +test('Add should error on missing value', () => { let item = { action: 'add', env: 'testing', name: 'foo', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.match(err.message, /Invalid value/, 'Errored on invalid value') - else t.fail('Expected invalid value error') + if (err) assert.match(err.message, /Invalid value/, 'Errored on invalid value') + else assert.fail('Expected invalid value error') }) }) -test('Add should treat all provided values as valid', t => { - let fake = sinon.fake.yields() +test('Add should treat all provided values as valid', () => { + let fake = mock.fn((params, callback) => callback()) aws.mock('SSM', 'putParameter', fake) let valids = [ 'http://foo.com/?bar=baz', @@ -92,7 +87,6 @@ test('Add should treat all provided values as valid', t => { '[${foo}]', '(%)^{}idk!', ] - t.plan(valids.length) series(valids.map(value => { return callback => { let item = { @@ -102,21 +96,20 @@ test('Add should treat all provided values as valid', t => { value } addRemove({ ...params, ...item }, function done (err) { - if (err) t.fail(err, 'Errored on valid values') + if (err) assert.fail(err, 'Errored on valid values') else { - t.pass('No error returned') + assert.ok(true, 'No error returned') callback() } }) } })) - sinon.restore() + mock.restoreAll() aws.restore('SSM') }) -test('Adding should callback with error if SSM errors', t => { - t.plan(1) - let fake = sinon.fake.yields({ boom: true }) +test('Adding should callback with error if SSM errors', () => { + let fake = mock.fn((params, callback) => callback({ boom: true })) aws.mock('SSM', 'putParameter', fake) let item = { action: 'add', @@ -125,15 +118,14 @@ test('Adding should callback with error if SSM errors', t => { value: 'bar', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.ok(err, 'Errored on SSM issue') - else t.fail('Expected SSM error') + if (err) assert.ok(err, 'Errored on SSM issue') + else assert.fail('Expected SSM error') }) aws.restore('SSM') }) -test('Remove should callback with error if SSM errors', t => { - t.plan(1) - let fake = sinon.fake.yields({ boom: true }) +test('Remove should callback with error if SSM errors', () => { + let fake = mock.fn((params, callback) => callback({ boom: true })) aws.mock('SSM', 'deleteParameter', fake) let item = { action: 'remove', @@ -141,15 +133,14 @@ test('Remove should callback with error if SSM errors', t => { name: 'foo', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.ok(err, 'Errored on SSM issue') - else t.fail('Expected SSM error') + if (err) assert.ok(err, 'Errored on SSM issue') + else assert.fail('Expected SSM error') }) aws.restore('SSM') }) -test('Remove should not callback with error if parameter is not found', t => { - t.plan(1) - let fake = sinon.fake.yields({ code: 'ParameterNotFound' }) +test('Remove should not callback with error if parameter is not found', () => { + let fake = mock.fn((params, callback) => callback({ code: 'ParameterNotFound' })) aws.mock('SSM', 'deleteParameter', fake) let item = { action: 'remove', @@ -157,8 +148,8 @@ test('Remove should not callback with error if parameter is not found', t => { name: 'foo', } addRemove({ ...params, ...item }, function done (err) { - if (err) t.fail('Should not have errored') - else t.pass(`No error returned`) + if (err) assert.fail('Should not have errored') + else assert.ok(true, 'No error returned') }) aws.restore('SSM') }) diff --git a/test/_all-tests.js b/test/_all-tests.js index 867428f..8ce917d 100644 --- a/test/_all-tests.js +++ b/test/_all-tests.js @@ -1,5 +1,5 @@ -/* let test = require('tape') -let sinon = require('sinon') +/* const { test, mock } = require('node:test') +const assert = require('node:assert') let AWS = require('aws-sdk') let aws = require('aws-sdk-mock') aws.setSDKInstance(AWS) @@ -12,38 +12,35 @@ let params = { inventory: { inv: { aws: { region: 'us-west-2' }, } }, update } -test('getEnv should callback with error if SSM errors', t => { - t.plan(1) - let fake = sinon.fake.yields({ boom: true }) +test('getEnv should callback with error if SSM errors', () => { + let fake = mock.fn((query, callback) => callback({ boom: true })) aws.mock('SSM', 'getParametersByPath', fake) getEnv(params, function done (err) { - if (err) t.ok(err, 'got an error when SSM explodes') - else t.fail('no error returned when SSM explodes') + if (err) assert.ok(err, 'got an error when SSM explodes') + else assert.fail('no error returned when SSM explodes') aws.restore('SSM') }) }) -test('getEnv should return massaged data from SSM', t => { - t.plan(1) - let fake = sinon.fake.yields(null, { +test('getEnv should return massaged data from SSM', () => { + let fake = mock.fn((query, callback) => callback(null, { Parameters: [ { Name: 'ssm/fakeappname/testing/key', Value: 'value' } ] - }) + })) aws.mock('SSM', 'getParametersByPath', fake) getEnv(params, function done (err, results) { if (err) { - t.fail('unexpected error callback when ssm returns proper data') + assert.fail('unexpected error callback when ssm returns proper data') console.log(err) } - else t.deepEqual(results, [ { app: 'appname', env: 'testing', name: 'key', value: 'value' } ], 'got expected format for SSM env vars') + else assert.deepStrictEqual(results, [ { app: 'appname', env: 'testing', name: 'key', value: 'value' } ], 'got expected format for SSM env vars') aws.restore('SSM') }) }) -test('getEnv should be able to handle paginated data from SSM', t => { - t.plan(2) - let fake = sinon.fake(function (query, callback) { +test('getEnv should be able to handle paginated data from SSM', () => { + let fake = mock.fn(function (query, callback) { // Only on the first call, provide a next token. - if (fake.callCount == 1) { + if (fake.mock.callCount() === 1) { callback(null, { Parameters: [ { Name: 'ssm/fakeappname/testing/key', Value: 'value' } ], NextToken: 'yep' @@ -56,12 +53,12 @@ test('getEnv should be able to handle paginated data from SSM', t => { aws.mock('SSM', 'getParametersByPath', fake) getEnv(params, function done (err, results) { if (err) { - t.fail('unexpected error') + assert.fail('unexpected error') console.log(err) } else { - t.equals(fake.callCount, 2, 'SSM.getParametersByPath called twice when next token is present') - t.equals(results.length, 2, 'returned results from both pages') + assert.strictEqual(fake.mock.callCount(), 2, 'SSM.getParametersByPath called twice when next token is present') + assert.strictEqual(results.length, 2, 'returned results from both pages') } aws.restore('SSM') }) diff --git a/test/_write-tests.js b/test/_write-tests.js index 01b3e0b..b6849c3 100644 --- a/test/_write-tests.js +++ b/test/_write-tests.js @@ -1,16 +1,15 @@ -let test = require('tape') -let sinon = require('sinon') +const { test, mock } = require('node:test') +const assert = require('node:assert') let write = require('../src/_write') let fs = require('fs') let { updater } = require('@architect/utils') let update = updater('Env') let params = { appname: 'fakeappname', update } -test('_write should write out env vars to a .arc-env file', t => { - t.plan(2) +test('_write should write out env vars to a .arc-env file', () => { process.env.ARC_TESTING = true - let fake = sinon.fake.returns() - sinon.replace(fs, 'writeFileSync', fake) + let fake = mock.fn() + mock.method(fs, 'writeFileSync', fake) write({ envVars: [ { env: 'testing', name: 'one', value: '1' }, { env: 'staging', name: 'two', value: '2' }, @@ -22,8 +21,8 @@ test('_write should write out env vars to a .arc-env file', t => { { env: 'production', name: 'dee', value: 'dee1.23' }, { env: 'production', name: 'eee', value: '1.23eee' }, ], ...params } ) - let args = fake.args - let file = args[0][1].split('\n').slice(1).join('\n') // Lop off the comment at the top of the block + let calls = fake.mock.calls + let file = calls[0].arguments[1].split('\n').slice(1).join('\n') // Lop off the comment at the top of the block let contents = `@env testing one 1 @@ -41,6 +40,7 @@ production eee 1.23eee ` delete process.env.ARC_TESTING - t.ok(args[0][0].endsWith('preferences.arc'), 'wrote to a file that ends in preferences.arc') - t.equal(file, contents, 'All env vars were placed correctly in the preferences file') + assert.ok(calls[0].arguments[0].endsWith('preferences.arc'), 'wrote to a file that ends in preferences.arc') + assert.strictEqual(file, contents, 'All env vars were placed correctly in the preferences file') + mock.restoreAll() }) diff --git a/test/index-tests.js b/test/index-tests.js index 510419a..36837b2 100644 --- a/test/index-tests.js +++ b/test/index-tests.js @@ -1,9 +1,9 @@ -let test = require('tape') -let proxyquire = require('proxyquire') +const { test } = require('node:test') +const assert = require('node:assert') let addRan = false let removeRan = false -let addRemove = (params, aws, callback) => { +let mockAddRemove = (params, aws, callback) => { let { action } = params if (action === 'add') addRan = true if (action === 'remove') removeRan = true @@ -11,16 +11,16 @@ let addRemove = (params, aws, callback) => { } let getEnvRan = false -let getEnv = (params, callback) => { +let mockGetEnv = (params, callback) => { getEnvRan = true callback(null, []) } let printRan = false -let print = () => ( printRan = true ) +let mockPrint = () => ( printRan = true ) let writeRan = false -let write = (params, callback) => { +let mockWrite = (params, callback) => { writeRan = true callback() } @@ -33,88 +33,102 @@ function reset () { writeRan = false } -let env = proxyquire('../', { - './_add-remove': addRemove, - './_get-env': getEnv, - './_print': print, - './_write': write, -}) +// Mock the internal modules by replacing them in the require cache +// Load the modules first to populate the cache +require('../src/_add-remove') +require('../src/_get-env') +require('../src/_print') +require('../src/_write') + +// Replace the module exports with mocks +require.cache[require.resolve('../src/_add-remove')].exports = mockAddRemove +require.cache[require.resolve('../src/_get-env')].exports = mockGetEnv +require.cache[require.resolve('../src/_print')].exports = mockPrint +require.cache[require.resolve('../src/_write')].exports = mockWrite + +let env = require('../src/index') let inventory = { inv: { app: 'fakename', aws: { region: 'us-west-2' }, } } -test('Set up env', t => { - t.plan(1) +test('Set up env', () => { process.env.AWS_ACCESS_KEY_ID = 'dummy' process.env.AWS_SECRET_ACCESS_KEY = 'dummy' - t.pass('Set up dummy creds') + assert.ok(true, 'Set up dummy creds') }) -test('Env errors if provided an unrecognized action', t => { - t.plan(1) - env({ action: 'idk', inventory }, function done (err) { - if (err) t.ok(err, 'got an error when env called with incorrect options') - else t.fail('no error returned when env called with incorrect options') +test('Env errors if provided an unrecognized action', async () => { + await new Promise((resolve) => { + env({ action: 'idk', inventory }, function done (err) { + if (err) assert.ok(err, 'got an error when env called with incorrect options') + else assert.fail('no error returned when env called with incorrect options') + resolve() + }) }) }) -test('Env prints and writes preferences on print', t => { - t.plan(3) - env({ action: 'print', inventory }, function done (err) { - if (err) { - t.fail('unexpected error when calling env with no parameters') - console.log(err) - } - else { - t.ok(getEnvRan, '`getEnv` invoked once') - t.ok(printRan, '`print` invoked once') - t.ok(writeRan, '`write` invoked once') - } - reset() +test('Env prints and writes preferences on print', async () => { + await new Promise((resolve) => { + env({ action: 'print', inventory }, function done (err) { + if (err) { + assert.fail('unexpected error when calling env with no parameters') + console.log(err) + } + else { + assert.ok(getEnvRan, '`getEnv` invoked once') + assert.ok(printRan, '`print` invoked once') + assert.ok(writeRan, '`write` invoked once') + } + reset() + resolve() + }) }) }) -test('Env invokes add, prints, and writes on a valid add request', t => { - t.plan(4) +test('Env invokes add, prints, and writes on a valid add request', async () => { let params = { action: 'add', env: 'production', name: 'foo', value: 'bar', inventory } - env(params, function done (err) { - if (err) { - t.fail('unexpected error when calling env with add parameters') - console.log(err) - } - else { - t.ok(getEnvRan, '`getEnv` invoked once') - t.ok(printRan, '`print` invoked once') - t.ok(writeRan, '`write` invoked once') - t.ok(addRan, '`add` invoked once') - } - reset() + await new Promise((resolve) => { + env(params, function done (err) { + if (err) { + assert.fail('unexpected error when calling env with add parameters') + console.log(err) + } + else { + assert.ok(getEnvRan, '`getEnv` invoked once') + assert.ok(printRan, '`print` invoked once') + assert.ok(writeRan, '`write` invoked once') + assert.ok(addRan, '`add` invoked once') + } + reset() + resolve() + }) }) }) -test('Env invokes remove, prints, and writes on a valid remove request', t => { - t.plan(4) +test('Env invokes remove, prints, and writes on a valid remove request', async () => { let params = { action: 'remove', env: 'production', name: 'foo', value: 'bar', inventory } - env(params, function done (err) { - if (err) { - t.fail('unexpected error when calling env with remove parameters') - console.log(err) - } - else { - t.ok(getEnvRan, '`getEnv` invoked once') - t.ok(printRan, '`print` invoked once') - t.ok(writeRan, '`write` invoked once') - t.ok(removeRan, '`remove` invoked once') - } - reset() + await new Promise((resolve) => { + env(params, function done (err) { + if (err) { + assert.fail('unexpected error when calling env with remove parameters') + console.log(err) + } + else { + assert.ok(getEnvRan, '`getEnv` invoked once') + assert.ok(printRan, '`print` invoked once') + assert.ok(writeRan, '`write` invoked once') + assert.ok(removeRan, '`remove` invoked once') + } + reset() + resolve() + }) }) }) -test('Tear down env', t => { - t.plan(1) +test('Tear down env', () => { delete process.env.AWS_ACCESS_KEY_ID delete process.env.AWS_SECRET_ACCESS_KEY - t.pass('Destroyed dummy creds') + assert.ok(true, 'Destroyed dummy creds') }) diff --git a/test/migration-verification-tests.js b/test/migration-verification-tests.js new file mode 100644 index 0000000..4068657 --- /dev/null +++ b/test/migration-verification-tests.js @@ -0,0 +1,173 @@ +const { test } = require('node:test') +const assert = require('node:assert') +const fs = require('fs') +const path = require('path') + +test('package.json does not contain removed dependencies', () => { + const packageJsonPath = path.join(__dirname, '..', 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + + const removedDeps = [ 'tape', 'nyc', 'tap-arc', 'cross-env', 'sinon', 'proxyquire' ] + const allDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + } + + removedDeps.forEach(dep => { + assert.ok(!allDeps[dep], `${dep} should not be in dependencies`) + }) +}) + +test('all test files use node:test and node:assert', () => { + const testDir = path.join(__dirname) + const testFiles = fs.readdirSync(testDir) + .filter(file => file.endsWith('-tests.js')) + .filter(file => file !== 'migration-verification-tests.js') // Skip this verification file + + assert.ok(testFiles.length > 0, 'should find test files') + + testFiles.forEach(file => { + const filePath = path.join(testDir, file) + const content = fs.readFileSync(filePath, 'utf8') + + // Check for node:test import + assert.ok( + content.includes("require('node:test')") || content.includes('require("node:test")'), + `${file} should import node:test`, + ) + + // Check for node:assert import + assert.ok( + content.includes("require('node:assert')") || content.includes('require("node:assert")'), + `${file} should import node:assert`, + ) + + // Should not import tape + assert.ok( + !content.includes("require('tape')") && !content.includes('require("tape")'), + `${file} should not import tape`, + ) + }) +}) + +test('no tape assertions remain in test files', () => { + const testDir = path.join(__dirname) + const testFiles = fs.readdirSync(testDir) + .filter(file => file.endsWith('-tests.js')) + .filter(file => file !== 'migration-verification-tests.js') // Skip this verification file + + const tapeAssertions = [ + /\bt\.plan\(/, + /\bt\.ok\(/, + /\bt\.equal\(/, + /\bt\.deepEqual\(/, + /\bt\.match\(/, + /\bt\.pass\(/, + /\bt\.fail\(/, + ] + + testFiles.forEach(file => { + const filePath = path.join(testDir, file) + const content = fs.readFileSync(filePath, 'utf8') + + // Remove comments to avoid false positives from commented code + const uncommentedContent = content + .split('\n') + .filter(line => !line.trim().startsWith('//')) + .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') + + tapeAssertions.forEach(assertion => { + assert.ok( + !assertion.test(uncommentedContent), + `${file} should not contain tape assertion ${assertion}`, + ) + }) + }) +}) + +test('no sinon or proxyquire usage remains in test files', () => { + const testDir = path.join(__dirname) + const testFiles = fs.readdirSync(testDir) + .filter(file => file.endsWith('-tests.js')) + .filter(file => file !== 'migration-verification-tests.js') // Skip this verification file + + testFiles.forEach(file => { + const filePath = path.join(testDir, file) + const content = fs.readFileSync(filePath, 'utf8') + + // Remove comments to avoid false positives from commented code + const uncommentedContent = content + .split('\n') + .filter(line => !line.trim().startsWith('//')) + .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, '') + + // Check for sinon usage + assert.ok( + !uncommentedContent.includes("require('sinon')") && !uncommentedContent.includes('require("sinon")'), + `${file} should not import sinon`, + ) + assert.ok( + !/\bsinon\./g.test(uncommentedContent), + `${file} should not use sinon methods`, + ) + + // Check for proxyquire usage + assert.ok( + !uncommentedContent.includes("require('proxyquire')") && !uncommentedContent.includes('require("proxyquire")'), + `${file} should not import proxyquire`, + ) + assert.ok( + !/\bproxyquire\(/g.test(uncommentedContent), + `${file} should not use proxyquire`, + ) + }) +}) + +test('npm scripts are correctly configured', () => { + const packageJsonPath = path.join(__dirname, '..', 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + + const scripts = packageJson.scripts + + // Verify test:unit script + assert.ok( + scripts['test:unit'].includes('node --test'), + 'test:unit should use node --test', + ) + assert.ok( + scripts['test:unit'].includes('test/**/*-tests.js'), + 'test:unit should use test/**/*-tests.js pattern', + ) + + // Verify coverage script + assert.ok( + scripts.coverage.includes('node --test'), + 'coverage should use node --test', + ) + assert.ok( + scripts.coverage.includes('--experimental-test-coverage'), + 'coverage should use --experimental-test-coverage flag', + ) + assert.ok( + scripts.coverage.includes('test/**/*-tests.js'), + 'coverage should use test/**/*-tests.js pattern', + ) + + // Verify test script + assert.ok( + scripts.test.includes('lint') && scripts.test.includes('coverage'), + 'test script should run lint and coverage', + ) + + // Verify test:nolint script + assert.ok( + scripts['test:nolint'].includes('coverage'), + 'test:nolint should run coverage', + ) + assert.ok( + !scripts['test:nolint'].includes('lint'), + 'test:nolint should not run lint', + ) +})