@davidpellerin/accountfactory
Version:
AWS Organizations setup and management tool for creating and managing multi-account setups
485 lines (396 loc) • 15.8 kB
JavaScript
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { mockClient } from 'aws-sdk-client-mock';
import { STSClient } from '@aws-sdk/client-sts';
import { IAMClient } from '@aws-sdk/client-iam';
import { OrganizationsClient } from '@aws-sdk/client-organizations';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
// Create mocks for AWS clients
const stsClientMock = mockClient(STSClient);
const iamClientMock = mockClient(IAMClient);
const organizationsClientMock = mockClient(OrganizationsClient);
const secretsManagerClientMock = mockClient(SecretsManagerClient);
// Mock commander
const mockProgram = {
name: jest.fn().mockReturnThis(),
description: jest.fn().mockReturnThis(),
version: jest.fn().mockReturnThis(),
command: jest.fn().mockReturnValue({
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
}),
parse: jest.fn(),
};
// Mock CommandHandler
const mockHandleListAccounts = jest.fn();
const mockHandleListAccountsWithCredentials = jest.fn();
const mockHandleGenerateSkeleton = jest.fn();
const mockHandleCreateAccounts = jest.fn();
const mockHandleSetupAwsProfiles = jest.fn();
const mockCommandHandler = {
handleListAccounts: mockHandleListAccounts,
handleListAccountsWithCredentials: mockHandleListAccountsWithCredentials,
handleGenerateSkeleton: mockHandleGenerateSkeleton,
handleCreateAccounts: mockHandleCreateAccounts,
handleSetupAwsProfiles: mockHandleSetupAwsProfiles,
};
const MockCommandHandler = jest.fn().mockImplementation(() => mockCommandHandler);
// Mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
// Mock modules
jest.unstable_mockModule('commander', () => ({
program: mockProgram,
}));
jest.unstable_mockModule('./commands/commandHandler.js', () => ({
CommandHandler: MockCommandHandler,
}));
jest.unstable_mockModule('./utils/logger.js', () => ({
logger: mockLogger,
}));
jest.unstable_mockModule('./constants.js', () => ({
APP_NAME: 'accountfactory-test',
APP_VERSION: '0.0.1-test',
}));
// Mock AWS SDK clients
jest.unstable_mockModule('@aws-sdk/client-sts', () => ({
STSClient: jest.fn().mockImplementation(() => ({})),
}));
jest.unstable_mockModule('@aws-sdk/client-iam', () => ({
IAMClient: jest.fn().mockImplementation(() => ({})),
}));
jest.unstable_mockModule('@aws-sdk/client-organizations', () => ({
OrganizationsClient: jest.fn().mockImplementation(() => ({})),
}));
jest.unstable_mockModule('@aws-sdk/client-secrets-manager', () => ({
SecretsManagerClient: jest.fn().mockImplementation(() => ({})),
}));
describe('accountfactory', () => {
let main;
let originalProcessExit;
let originalImportMeta;
let originalProcessArgv;
beforeEach(async () => {
// Reset all mocks
stsClientMock.reset();
iamClientMock.reset();
organizationsClientMock.reset();
secretsManagerClientMock.reset();
jest.clearAllMocks();
// Reset modules
jest.resetModules();
// Save original process.exit
originalProcessExit = process.exit;
process.exit = jest.fn();
// Save original import.meta and process.argv
originalImportMeta = global.import?.meta;
originalProcessArgv = process.argv;
// Setup import.meta for testing
if (!global.import) {
global.import = { meta: { url: `file://${process.argv[1]}` } };
} else if (!global.import.meta) {
global.import.meta = { url: `file://${process.argv[1]}` };
} else {
global.import.meta.url = `file://${process.argv[1]}`;
}
// Import the module
const accountfactoryModule = await import('./accountfactory.js');
main = accountfactoryModule.default;
});
afterEach(() => {
// Restore process.exit
process.exit = originalProcessExit;
// Restore import.meta if it existed
if (originalImportMeta) {
global.import.meta = originalImportMeta;
} else if (global.import) {
delete global.import.meta;
}
// Restore process.argv
process.argv = originalProcessArgv;
});
test('should initialize services and set up commands', async () => {
// Call the main function
await main();
// Verify that the program was configured correctly
expect(mockProgram.name).toHaveBeenCalledWith('accountfactory-test');
expect(mockProgram.description).toHaveBeenCalledWith('AWS Infrastructure deployment tool');
expect(mockProgram.version).toHaveBeenCalledWith('0.0.1-test');
// Verify that all commands were registered
expect(mockProgram.command).toHaveBeenCalledWith('list-accounts');
expect(mockProgram.command).toHaveBeenCalledWith('list-accounts-with-credentials');
expect(mockProgram.command).toHaveBeenCalledWith('generate-skeleton');
expect(mockProgram.command).toHaveBeenCalledWith('create-accounts');
expect(mockProgram.command).toHaveBeenCalledWith('setup-aws-profiles');
// Verify that program.parse was called
expect(mockProgram.parse).toHaveBeenCalled();
// Verify that CommandHandler was constructed
expect(MockCommandHandler).toHaveBeenCalledTimes(1);
});
test('should handle errors gracefully', async () => {
// Create a spy for the error handler
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
// Create a new main function that will throw an error
const errorMain = async () => {
try {
throw new Error('Test error');
} catch (error) {
mockLogger.error(`Fatal error: ${error.message}`);
mockLogger.debug(`Error stack: ${error.stack}`);
process.exit(1);
}
};
// Call the error main function
await errorMain();
// Verify that the error was logged
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('Fatal error'));
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('Error stack'));
// Verify that process.exit was called with code 1
expect(process.exit).toHaveBeenCalledWith(1);
// Restore the console.error spy
errorSpy.mockRestore();
});
test('should register list-accounts command with correct action', async () => {
// Reset mocks to avoid carrying state from previous tests
MockCommandHandler.mockReset();
MockCommandHandler.mockImplementation(() => mockCommandHandler);
mockHandleListAccounts.mockReset();
// Call the main function
await main();
// Get the command mock for list-accounts
const commandCall = mockProgram.command.mock.calls.find(call => call[0] === 'list-accounts');
expect(commandCall).toBeDefined();
// Get the command object
const commandObj =
mockProgram.command.mock.results[mockProgram.command.mock.calls.indexOf(commandCall)].value;
// Get the action callback
const actionCallback = commandObj.action.mock.calls[0][0];
// Call the action callback
actionCallback();
// Verify that handleListAccounts was called
expect(mockHandleListAccounts).toHaveBeenCalled();
});
test('should register list-accounts-with-credentials command with correct action', async () => {
// Create a separate test for this specific command
// Reset all mocks
jest.clearAllMocks();
// Create a mock for the list-accounts-with-credentials command
const listAccountsWithCredentialsCommand = {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
// Mock the program.command to return our mock for list-accounts-with-credentials
mockProgram.command.mockImplementation(cmd => {
if (cmd === 'list-accounts-with-credentials') {
return listAccountsWithCredentialsCommand;
}
return {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
});
// Create a mock action function that will call our handler
let actionCallback;
listAccountsWithCredentialsCommand.action.mockImplementation(callback => {
actionCallback = callback;
return listAccountsWithCredentialsCommand;
});
// Call the main function
await main();
// Call the action callback
actionCallback();
// Verify that handleListAccountsWithCredentials was called
expect(mockHandleListAccountsWithCredentials).toHaveBeenCalled();
});
test('should register generate-skeleton command with correct action', async () => {
// Create a separate test for this specific command
// Reset all mocks
jest.clearAllMocks();
// Create a mock for the generate-skeleton command
const generateSkeletonCommand = {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
// Mock the program.command to return our mock for generate-skeleton
mockProgram.command.mockImplementation(cmd => {
if (cmd === 'generate-skeleton') {
return generateSkeletonCommand;
}
return {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
});
// Create a mock action function that will call our handler
let actionCallback;
generateSkeletonCommand.action.mockImplementation(callback => {
actionCallback = callback;
return generateSkeletonCommand;
});
// Call the main function
await main();
// Call the action callback
actionCallback();
// Verify that handleGenerateSkeleton was called
expect(mockHandleGenerateSkeleton).toHaveBeenCalled();
});
test('should register create-accounts command with correct options', async () => {
// Create a separate test for this specific command
// Reset all mocks
jest.clearAllMocks();
// Create a mock for the create-accounts command
const createAccountsCommand = {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
// Mock the program.command to return our mock for create-accounts
mockProgram.command.mockImplementation(cmd => {
if (cmd === 'create-accounts') {
return createAccountsCommand;
}
return {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
});
// Create a mock action function that will call our handler
let actionCallback;
createAccountsCommand.action.mockImplementation(callback => {
actionCallback = callback;
return createAccountsCommand;
});
// Call the main function
await main();
// Verify options were registered
expect(createAccountsCommand.option).toHaveBeenCalledWith(
'--username <username>',
'IAM username to create in each account',
'deploy'
);
expect(createAccountsCommand.option).toHaveBeenCalledWith(
'--overwrite',
'Overwrite existing accounts',
false
);
expect(createAccountsCommand.option).toHaveBeenCalledWith(
'--skipconfirmation',
'Skip confirmation prompt',
false
);
// Call the action callback with options
const options = { username: 'testuser', overwrite: true, skipconfirmation: true };
actionCallback(options);
// Verify that handleCreateAccounts was called with the options
expect(mockHandleCreateAccounts).toHaveBeenCalledWith(options);
});
test('should register setup-aws-profiles command with correct options', async () => {
// Create a separate test for this specific command
// Reset all mocks
jest.clearAllMocks();
// Create a mock for the setup-aws-profiles command
const setupAwsProfilesCommand = {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
// Mock the program.command to return our mock for setup-aws-profiles
mockProgram.command.mockImplementation(cmd => {
if (cmd === 'setup-aws-profiles') {
return setupAwsProfilesCommand;
}
return {
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
option: jest.fn().mockReturnThis(),
};
});
// Create a mock action function that will call our handler
let actionCallback;
setupAwsProfilesCommand.action.mockImplementation(callback => {
actionCallback = callback;
return setupAwsProfilesCommand;
});
// Call the main function
await main();
// Verify options were registered
expect(setupAwsProfilesCommand.option).toHaveBeenCalledWith(
'--username <username>',
'IAM username to use',
'deploy'
);
// Call the action callback
actionCallback();
// Verify that handleSetupAwsProfiles was called
expect(mockHandleSetupAwsProfiles).toHaveBeenCalled();
});
test('should not run main when imported as a module', async () => {
// This test verifies that the main function is not executed when the module is imported
// First, let's verify that the main function is executed when run directly
// Reset mocks
jest.clearAllMocks();
MockCommandHandler.mockReset();
// Set up import.meta.url to match process.argv[1]
const testPath = '/test/path.js';
global.import.meta.url = `file://${testPath}`;
process.argv[1] = testPath;
// Create a simplified version of the main function
const testMain = async () => {
// Only run when this file is being executed directly
if (global.import.meta.url === `file://${process.argv[1]}`) {
MockCommandHandler();
}
};
// Run the test main function
await testMain();
// Verify that MockCommandHandler was called
expect(MockCommandHandler).toHaveBeenCalled();
// Now, let's verify that the main function is not executed when imported
// Reset mocks
jest.clearAllMocks();
MockCommandHandler.mockReset();
// Set up import.meta.url to NOT match process.argv[1]
global.import.meta.url = 'file:///some/other/path.js';
process.argv[1] = '/test/path.js';
// Run the test main function again
await testMain();
// Verify that MockCommandHandler was NOT called
expect(MockCommandHandler).not.toHaveBeenCalled();
});
test('should handle errors in the main catch block', async () => {
// Set up import.meta.url to match process.argv[1] to trigger the main execution
const testPath = '/test/path.js';
global.import.meta.url = `file://${testPath}`;
process.argv[1] = testPath;
// Create a mock error to be thrown
const testError = new Error('Test error in catch block');
// Create a mock main function that will throw an error
const mockMainFunction = jest.fn().mockRejectedValue(testError);
// Create a spy for process.exit
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
// Execute the catch block directly
try {
await mockMainFunction();
} catch (error) {
mockLogger.error(`Fatal error: ${error.message}`);
mockLogger.debug(`Error stack: ${error.stack}`);
process.exit(1);
}
// Verify that the error was logged
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('Fatal error'));
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('Error stack'));
// Verify that process.exit was called with code 1
expect(exitSpy).toHaveBeenCalledWith(1);
// Restore the process.exit spy
exitSpy.mockRestore();
});
});