@friggframework/frigg-cli
Version:
Frigg Framework CLI tool
297 lines (225 loc) • 10.5 kB
JavaScript
/**
* Test suite for the frigg start command
*
* These tests ensure that the start command properly:
* 1. Sets FRIGG_SKIP_AWS_DISCOVERY=true in the parent process to skip AWS API calls
* 2. Suppresses AWS SDK maintenance mode warnings
* 3. Spawns serverless with correct configuration
* 4. Validates database configuration before starting
*
* This fixes the issue where frigg start would attempt AWS discovery during local development,
* causing unnecessary AWS API calls and potential failures when AWS credentials aren't available.
*/
// Mock dependencies BEFORE importing startCommand
const mockValidator = {
validateDatabaseUrl: jest.fn(),
getDatabaseType: jest.fn(),
checkPrismaClientGenerated: jest.fn()
};
jest.mock('node:child_process', () => ({
spawn: jest.fn(),
}));
jest.mock('../utils/database-validator', () => mockValidator);
jest.mock('dotenv');
const { spawn } = require('node:child_process');
const { startCommand } = require('./index');
const { createMockDatabaseValidator } = require('../__tests__/utils/prisma-mock');
const dotenv = require('dotenv');
describe('startCommand', () => {
let mockChildProcess;
let mockProcessExit;
beforeEach(() => {
// Mock process.exit to throw error and stop execution (prevents actual exits)
const exitError = new Error('process.exit called');
exitError.code = 'PROCESS_EXIT';
mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw exitError;
});
// Reset mocks
jest.clearAllMocks();
// Re-apply process.exit mock after clearAllMocks
mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw exitError;
});
// Set up default database validator mocks for all tests
const defaultValidator = createMockDatabaseValidator();
mockValidator.validateDatabaseUrl.mockReturnValue(defaultValidator.validateDatabaseUrl());
mockValidator.getDatabaseType.mockReturnValue(defaultValidator.getDatabaseType());
mockValidator.checkPrismaClientGenerated.mockReturnValue(defaultValidator.checkPrismaClientGenerated());
// Mock dotenv
dotenv.config = jest.fn();
// Clear environment variables
delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
// Create a mock child process
mockChildProcess = {
on: jest.fn(),
stdout: { on: jest.fn() },
stderr: { on: jest.fn() },
};
spawn.mockReturnValue(mockChildProcess);
});
afterEach(() => {
// Restore process.exit
if (mockProcessExit) {
mockProcessExit.mockRestore();
}
// Clean up environment
delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
});
it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', async () => {
const options = { stage: 'dev' };
await startCommand(options);
// Verify the environment variable is set in the parent process
expect(process.env.FRIGG_SKIP_AWS_DISCOVERY).toBe('true');
});
it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', async () => {
const options = { stage: 'dev' };
await startCommand(options);
expect(process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE).toBe('1');
});
it('should spawn serverless with correct arguments', async () => {
const options = { stage: 'prod' };
await startCommand(options);
expect(spawn).toHaveBeenCalledWith(
'osls',
['offline', '--config', 'infrastructure.js', '--stage', 'prod'],
expect.objectContaining({
cwd: expect.any(String),
stdio: 'inherit',
env: expect.objectContaining({
FRIGG_SKIP_AWS_DISCOVERY: 'true',
}),
})
);
});
it('should include verbose flag when verbose option is enabled', async () => {
const options = { stage: 'dev', verbose: true };
await startCommand(options);
expect(spawn).toHaveBeenCalledWith(
'osls',
['offline', '--config', 'infrastructure.js', '--stage', 'dev', '--verbose'],
expect.any(Object)
);
});
it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', async () => {
const options = { stage: 'dev' };
await startCommand(options);
const spawnCall = spawn.mock.calls[0];
const spawnOptions = spawnCall[2];
expect(spawnOptions.env).toHaveProperty('FRIGG_SKIP_AWS_DISCOVERY', 'true');
});
it('should handle child process errors', async () => {
const options = { stage: 'dev' };
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
await startCommand(options);
// Simulate an error
const errorCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'error')[1];
const testError = new Error('Test error');
errorCallback(testError);
expect(consoleErrorSpy).toHaveBeenCalledWith('Error executing command: Test error');
consoleErrorSpy.mockRestore();
});
it('should handle child process exit with non-zero code', async () => {
const options = { stage: 'dev' };
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
await startCommand(options);
// Simulate exit with error code
const closeCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'close')[1];
closeCallback(1);
expect(consoleLogSpy).toHaveBeenCalledWith('Child process exited with code 1');
consoleLogSpy.mockRestore();
});
it('should not log on successful exit', async () => {
const options = { stage: 'dev' };
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
await startCommand(options);
// Clear the spy calls from startCommand execution
consoleLogSpy.mockClear();
// Simulate successful exit
const closeCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'close')[1];
closeCallback(0);
// Should not log anything for successful exit
expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('exited'));
consoleLogSpy.mockRestore();
});
describe('Database Pre-flight Validation', () => {
let mockConsoleError;
beforeEach(() => {
// Mock console.error (all other mocks are set up in outer beforeEach)
mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
});
afterEach(() => {
mockConsoleError.mockRestore();
});
it('should pass pre-flight checks when database valid', async () => {
const options = { stage: 'dev' };
await startCommand(options);
expect(mockValidator.validateDatabaseUrl).toHaveBeenCalled();
expect(mockValidator.getDatabaseType).toHaveBeenCalled();
expect(mockValidator.checkPrismaClientGenerated).toHaveBeenCalled();
expect(mockProcessExit).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalled();
});
it('should fail when DATABASE_URL missing', async () => {
mockValidator.validateDatabaseUrl.mockReturnValue({
valid: false,
error: 'DATABASE_URL not found'
});
await expect(startCommand({})).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalled();
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(spawn).not.toHaveBeenCalled();
});
it('should fail when database type not configured', async () => {
mockValidator.getDatabaseType.mockReturnValue({
error: 'Database not configured'
});
await expect(startCommand({})).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalled();
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(spawn).not.toHaveBeenCalled();
});
it('should fail when Prisma client not generated', async () => {
mockValidator.checkPrismaClientGenerated.mockReturnValue({
generated: false,
error: 'Client not found'
});
await expect(startCommand({})).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalled();
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(spawn).not.toHaveBeenCalled();
});
it('should suggest running frigg db:setup when client missing', async () => {
mockValidator.checkPrismaClientGenerated.mockReturnValue({
generated: false,
error: 'Client not generated'
});
await expect(startCommand({})).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
});
it('should exit with code 1 on validation failure', async () => {
mockValidator.validateDatabaseUrl.mockReturnValue({
valid: false
});
await expect(startCommand({})).rejects.toThrow('process.exit called');
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('should continue to serverless start when validation passes', async () => {
await startCommand({ stage: 'dev' });
expect(spawn).toHaveBeenCalledWith(
'osls',
expect.arrayContaining(['offline']),
expect.any(Object)
);
});
it('should load .env before validation', async () => {
await startCommand({});
expect(dotenv.config).toHaveBeenCalledWith(expect.objectContaining({
path: expect.stringContaining('.env')
}));
});
});
});