@friggframework/devtools 2.0.0--canary.395.93acf57.0 ā 2.0.0--canary.395.04851d8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/__tests__/unit/commands/build.test.js +173 -405
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +436 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +359 -377
- package/frigg-cli/__tests__/unit/commands/ui.test.js +266 -512
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +486 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-setup.js +22 -21
- package/frigg-cli/db-setup-command/index.js +179 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +151 -162
- package/frigg-cli/generate-iam-command.js +7 -4
- package/frigg-cli/index.js +9 -1
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +4 -1
- package/frigg-cli/start-command/index.js +108 -2
- package/frigg-cli/start-command/start-command.test.js +177 -19
- package/frigg-cli/utils/database-validator.js +158 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/prisma-runner.js +278 -0
- package/infrastructure/IAM-POLICY-TEMPLATES.md +30 -12
- package/infrastructure/iam-generator.js +18 -38
- package/infrastructure/iam-generator.test.js +40 -8
- package/package.json +6 -6
- package/frigg-cli/__tests__/jest.config.js +0 -102
- package/frigg-cli/__tests__/utils/command-tester.js +0 -170
|
@@ -55,12 +55,15 @@ async function generateIamCommand(options = {}) {
|
|
|
55
55
|
|
|
56
56
|
// Generate the CloudFormation template
|
|
57
57
|
console.log('\\nšļø Generating IAM CloudFormation template...');
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
const deploymentUserName = options.user || 'frigg-deployment-user';
|
|
60
60
|
const stackName = options.stackName || 'frigg-deployment-iam';
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
|
|
62
|
+
// Use the summary already extracted above (line 44)
|
|
63
|
+
const cloudFormationYaml = generateIAMCloudFormation({
|
|
64
|
+
appName: summary.appName,
|
|
65
|
+
features: summary.features,
|
|
66
|
+
userPrefix: deploymentUserName,
|
|
64
67
|
stackName
|
|
65
68
|
});
|
|
66
69
|
|
package/frigg-cli/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { buildCommand } = require('./build-command');
|
|
|
8
8
|
const { deployCommand } = require('./deploy-command');
|
|
9
9
|
const { generateIamCommand } = require('./generate-iam-command');
|
|
10
10
|
const { uiCommand } = require('./ui-command');
|
|
11
|
+
const { dbSetupCommand } = require('./db-setup-command');
|
|
11
12
|
|
|
12
13
|
const program = new Command();
|
|
13
14
|
|
|
@@ -61,6 +62,13 @@ program
|
|
|
61
62
|
.option('--no-open', 'do not open browser automatically')
|
|
62
63
|
.action(uiCommand);
|
|
63
64
|
|
|
65
|
+
program
|
|
66
|
+
.command('db:setup')
|
|
67
|
+
.description('Set up database schema and generate Prisma client')
|
|
68
|
+
.option('-s, --stage <stage>', 'deployment stage', 'development')
|
|
69
|
+
.option('-v, --verbose', 'enable verbose output')
|
|
70
|
+
.action(dbSetupCommand);
|
|
71
|
+
|
|
64
72
|
program.parse(process.argv);
|
|
65
73
|
|
|
66
|
-
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand };
|
|
74
|
+
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
displayName: 'Frigg CLI Tests',
|
|
3
|
+
testMatch: [
|
|
4
|
+
'<rootDir>/__tests__/**/*.test.js',
|
|
5
|
+
'<rootDir>/__tests__/**/*.spec.js',
|
|
6
|
+
'<rootDir>/**/start-command.test.js',
|
|
7
|
+
'<rootDir>/**/__tests__/**/*.test.js'
|
|
8
|
+
],
|
|
9
|
+
// Exclude utility files and config from being treated as tests
|
|
10
|
+
testPathIgnorePatterns: [
|
|
11
|
+
'/node_modules/',
|
|
12
|
+
'/__tests__/utils/',
|
|
13
|
+
'/__tests__/jest.config.js',
|
|
14
|
+
'/test-setup.js'
|
|
15
|
+
],
|
|
16
|
+
testEnvironment: 'node',
|
|
17
|
+
collectCoverageFrom: [
|
|
18
|
+
'**/*.js',
|
|
19
|
+
'!**/*.test.js',
|
|
20
|
+
'!**/*.spec.js',
|
|
21
|
+
'!**/node_modules/**',
|
|
22
|
+
'!**/__tests__/**',
|
|
23
|
+
'!**/coverage/**'
|
|
24
|
+
],
|
|
25
|
+
coverageDirectory: 'coverage',
|
|
26
|
+
coverageReporters: [
|
|
27
|
+
'text',
|
|
28
|
+
'text-summary',
|
|
29
|
+
'html',
|
|
30
|
+
'lcov',
|
|
31
|
+
'json'
|
|
32
|
+
],
|
|
33
|
+
coverageThreshold: {
|
|
34
|
+
global: {
|
|
35
|
+
branches: 85,
|
|
36
|
+
functions: 85,
|
|
37
|
+
lines: 85,
|
|
38
|
+
statements: 85
|
|
39
|
+
},
|
|
40
|
+
'./install-command/index.js': {
|
|
41
|
+
branches: 90,
|
|
42
|
+
functions: 90,
|
|
43
|
+
lines: 90,
|
|
44
|
+
statements: 90
|
|
45
|
+
},
|
|
46
|
+
'./build-command/index.js': {
|
|
47
|
+
branches: 90,
|
|
48
|
+
functions: 90,
|
|
49
|
+
lines: 90,
|
|
50
|
+
statements: 90
|
|
51
|
+
},
|
|
52
|
+
'./deploy-command/index.js': {
|
|
53
|
+
branches: 90,
|
|
54
|
+
functions: 90,
|
|
55
|
+
lines: 90,
|
|
56
|
+
statements: 90
|
|
57
|
+
},
|
|
58
|
+
'./ui-command/index.js': {
|
|
59
|
+
branches: 90,
|
|
60
|
+
functions: 90,
|
|
61
|
+
lines: 90,
|
|
62
|
+
statements: 90
|
|
63
|
+
},
|
|
64
|
+
'./generate-command/index.js': {
|
|
65
|
+
branches: 90,
|
|
66
|
+
functions: 90,
|
|
67
|
+
lines: 90,
|
|
68
|
+
statements: 90
|
|
69
|
+
},
|
|
70
|
+
'./db-setup-command/index.js': {
|
|
71
|
+
branches: 90,
|
|
72
|
+
functions: 90,
|
|
73
|
+
lines: 90,
|
|
74
|
+
statements: 90
|
|
75
|
+
},
|
|
76
|
+
'./utils/database-validator.js': {
|
|
77
|
+
branches: 85,
|
|
78
|
+
functions: 85,
|
|
79
|
+
lines: 85,
|
|
80
|
+
statements: 85
|
|
81
|
+
},
|
|
82
|
+
'./utils/prisma-runner.js': {
|
|
83
|
+
branches: 85,
|
|
84
|
+
functions: 85,
|
|
85
|
+
lines: 85,
|
|
86
|
+
statements: 85
|
|
87
|
+
},
|
|
88
|
+
'./utils/error-messages.js': {
|
|
89
|
+
branches: 85,
|
|
90
|
+
functions: 85,
|
|
91
|
+
lines: 85,
|
|
92
|
+
statements: 85
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
setupFilesAfterEnv: [
|
|
96
|
+
'<rootDir>/__tests__/utils/test-setup.js'
|
|
97
|
+
],
|
|
98
|
+
testTimeout: 10000,
|
|
99
|
+
maxWorkers: '50%',
|
|
100
|
+
verbose: true,
|
|
101
|
+
collectCoverage: true,
|
|
102
|
+
coveragePathIgnorePatterns: [
|
|
103
|
+
'/node_modules/',
|
|
104
|
+
'/__tests__/',
|
|
105
|
+
'/coverage/',
|
|
106
|
+
'.test.js',
|
|
107
|
+
'.spec.js'
|
|
108
|
+
],
|
|
109
|
+
moduleFileExtensions: [
|
|
110
|
+
'js',
|
|
111
|
+
'json',
|
|
112
|
+
'node'
|
|
113
|
+
],
|
|
114
|
+
transform: {},
|
|
115
|
+
// testResultsProcessor: 'jest-sonar-reporter', // Optional dependency
|
|
116
|
+
reporters: [
|
|
117
|
+
'default'
|
|
118
|
+
// jest-junit reporter removed - optional dependency
|
|
119
|
+
],
|
|
120
|
+
watchman: false,
|
|
121
|
+
forceExit: true,
|
|
122
|
+
detectOpenHandles: true,
|
|
123
|
+
errorOnDeprecated: true
|
|
124
|
+
};
|
package/frigg-cli/package.json
CHANGED
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@babel/parser": "^7.25.3",
|
|
14
14
|
"@babel/traverse": "^7.25.3",
|
|
15
|
+
"@friggframework/core": "workspace:*",
|
|
15
16
|
"@friggframework/schemas": "workspace:*",
|
|
16
17
|
"@inquirer/prompts": "^5.3.8",
|
|
17
18
|
"axios": "^1.7.2",
|
|
18
19
|
"chalk": "^4.1.2",
|
|
19
20
|
"commander": "^12.1.0",
|
|
20
21
|
"cross-spawn": "^7.0.3",
|
|
22
|
+
"dotenv": "^16.4.5",
|
|
21
23
|
"fs-extra": "^11.2.0",
|
|
22
24
|
"js-yaml": "^4.1.0",
|
|
23
25
|
"lodash": "4.17.21",
|
|
@@ -27,7 +29,8 @@
|
|
|
27
29
|
"validate-npm-package-name": "^5.0.0"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
|
-
"jest": "^29.7.0"
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"jest-mock-extended": "^3.0.5"
|
|
31
34
|
},
|
|
32
35
|
"keywords": [
|
|
33
36
|
"frigg",
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
const { spawn } = require('node:child_process');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const dotenv = require('dotenv');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const {
|
|
6
|
+
validateDatabaseUrl,
|
|
7
|
+
getDatabaseType,
|
|
8
|
+
testDatabaseConnection,
|
|
9
|
+
checkPrismaClientGenerated
|
|
10
|
+
} = require('../utils/database-validator');
|
|
11
|
+
const {
|
|
12
|
+
getDatabaseUrlMissingError,
|
|
13
|
+
getDatabaseTypeNotConfiguredError,
|
|
14
|
+
getDatabaseConnectionError,
|
|
15
|
+
getPrismaClientNotGeneratedError
|
|
16
|
+
} = require('../utils/error-messages');
|
|
3
17
|
|
|
4
|
-
function startCommand(options) {
|
|
18
|
+
async function startCommand(options) {
|
|
5
19
|
if (options.verbose) {
|
|
6
20
|
console.log('Verbose mode enabled');
|
|
7
21
|
console.log('Options:', options);
|
|
8
22
|
}
|
|
23
|
+
|
|
24
|
+
console.log(chalk.blue('š Starting Frigg application...\n'));
|
|
25
|
+
|
|
26
|
+
// Load environment variables from .env file
|
|
27
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
28
|
+
dotenv.config({ path: envPath });
|
|
29
|
+
|
|
30
|
+
// Pre-flight database checks
|
|
31
|
+
try {
|
|
32
|
+
await performDatabaseChecks(options.verbose);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(chalk.red('\nā Pre-flight checks failed'));
|
|
35
|
+
console.error(chalk.gray('Fix the issues above before starting the application.\n'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.green('ā Database checks passed\n'));
|
|
9
40
|
console.log('Starting backend and optional frontend...');
|
|
41
|
+
|
|
10
42
|
// Suppress AWS SDK warning message about maintenance mode
|
|
11
|
-
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = 1;
|
|
43
|
+
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = '1';
|
|
12
44
|
// Skip AWS discovery for local development
|
|
13
45
|
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
14
46
|
const backendPath = path.resolve(process.cwd());
|
|
@@ -53,4 +85,78 @@ function startCommand(options) {
|
|
|
53
85
|
});
|
|
54
86
|
}
|
|
55
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Performs pre-flight database validation checks
|
|
90
|
+
* @param {boolean} verbose - Enable verbose output
|
|
91
|
+
* @throws {Error} If any validation check fails
|
|
92
|
+
*/
|
|
93
|
+
async function performDatabaseChecks(verbose) {
|
|
94
|
+
// Check 1: Validate DATABASE_URL exists
|
|
95
|
+
if (verbose) {
|
|
96
|
+
console.log(chalk.gray('Checking DATABASE_URL...'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const urlValidation = validateDatabaseUrl();
|
|
100
|
+
if (!urlValidation.valid) {
|
|
101
|
+
console.error(getDatabaseUrlMissingError());
|
|
102
|
+
throw new Error('DATABASE_URL validation failed');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (verbose) {
|
|
106
|
+
console.log(chalk.green('ā DATABASE_URL found'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check 2: Determine database type
|
|
110
|
+
if (verbose) {
|
|
111
|
+
console.log(chalk.gray('Determining database type...'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const dbTypeResult = getDatabaseType();
|
|
115
|
+
if (dbTypeResult.error) {
|
|
116
|
+
console.error(chalk.red('ā ' + dbTypeResult.error));
|
|
117
|
+
console.error(getDatabaseTypeNotConfiguredError());
|
|
118
|
+
throw new Error('Database type determination failed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dbType = dbTypeResult.dbType;
|
|
122
|
+
|
|
123
|
+
if (verbose) {
|
|
124
|
+
console.log(chalk.green(`ā Database type: ${dbType}`));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check 3: Verify Prisma client is generated (BEFORE connection test to prevent auto-generation)
|
|
128
|
+
if (verbose) {
|
|
129
|
+
console.log(chalk.gray('Checking Prisma client...'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const clientCheck = checkPrismaClientGenerated(dbType);
|
|
133
|
+
|
|
134
|
+
if (!clientCheck.generated) {
|
|
135
|
+
console.error(getPrismaClientNotGeneratedError(dbType));
|
|
136
|
+
console.error(chalk.yellow('\nRun this command to generate the Prisma client:'));
|
|
137
|
+
console.error(chalk.cyan(' frigg db:setup\n'));
|
|
138
|
+
throw new Error('Prisma client not generated');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (verbose) {
|
|
142
|
+
console.log(chalk.green('ā Prisma client generated'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check 4: Test database connection (only after confirming client exists)
|
|
146
|
+
if (verbose) {
|
|
147
|
+
console.log(chalk.gray('Testing database connection...'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const connectionTest = await testDatabaseConnection(urlValidation.url, dbType, 5000);
|
|
151
|
+
|
|
152
|
+
if (!connectionTest.connected) {
|
|
153
|
+
console.error(getDatabaseConnectionError(connectionTest.error, dbType));
|
|
154
|
+
throw new Error('Database connection failed');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (verbose) {
|
|
158
|
+
console.log(chalk.green('ā Database connection verified'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
56
162
|
module.exports = { startCommand };
|
|
@@ -5,26 +5,62 @@
|
|
|
5
5
|
* 1. Sets FRIGG_SKIP_AWS_DISCOVERY=true in the parent process to skip AWS API calls
|
|
6
6
|
* 2. Suppresses AWS SDK maintenance mode warnings
|
|
7
7
|
* 3. Spawns serverless with correct configuration
|
|
8
|
+
* 4. Validates database configuration before starting
|
|
8
9
|
*
|
|
9
10
|
* This fixes the issue where frigg start would attempt AWS discovery during local development,
|
|
10
11
|
* causing unnecessary AWS API calls and potential failures when AWS credentials aren't available.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
const
|
|
14
|
+
// Mock dependencies BEFORE importing startCommand
|
|
15
|
+
const mockValidator = {
|
|
16
|
+
validateDatabaseUrl: jest.fn(),
|
|
17
|
+
getDatabaseType: jest.fn(),
|
|
18
|
+
testDatabaseConnection: jest.fn(),
|
|
19
|
+
checkPrismaClientGenerated: jest.fn()
|
|
20
|
+
};
|
|
15
21
|
|
|
16
|
-
// Mock the spawn function
|
|
17
22
|
jest.mock('node:child_process', () => ({
|
|
18
23
|
spawn: jest.fn(),
|
|
19
24
|
}));
|
|
20
25
|
|
|
26
|
+
jest.mock('../utils/database-validator', () => mockValidator);
|
|
27
|
+
jest.mock('dotenv');
|
|
28
|
+
|
|
29
|
+
const { spawn } = require('node:child_process');
|
|
30
|
+
const { startCommand } = require('./index');
|
|
31
|
+
const { createMockDatabaseValidator } = require('../__tests__/utils/prisma-mock');
|
|
32
|
+
const dotenv = require('dotenv');
|
|
33
|
+
|
|
21
34
|
describe('startCommand', () => {
|
|
22
35
|
let mockChildProcess;
|
|
36
|
+
let mockProcessExit;
|
|
23
37
|
|
|
24
38
|
beforeEach(() => {
|
|
39
|
+
// Mock process.exit to throw error and stop execution (prevents actual exits)
|
|
40
|
+
const exitError = new Error('process.exit called');
|
|
41
|
+
exitError.code = 'PROCESS_EXIT';
|
|
42
|
+
mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
43
|
+
throw exitError;
|
|
44
|
+
});
|
|
45
|
+
|
|
25
46
|
// Reset mocks
|
|
26
47
|
jest.clearAllMocks();
|
|
27
48
|
|
|
49
|
+
// Re-apply process.exit mock after clearAllMocks
|
|
50
|
+
mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
51
|
+
throw exitError;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Set up default database validator mocks for all tests
|
|
55
|
+
const defaultValidator = createMockDatabaseValidator();
|
|
56
|
+
mockValidator.validateDatabaseUrl.mockReturnValue(defaultValidator.validateDatabaseUrl());
|
|
57
|
+
mockValidator.getDatabaseType.mockReturnValue(defaultValidator.getDatabaseType());
|
|
58
|
+
mockValidator.testDatabaseConnection.mockResolvedValue(defaultValidator.testDatabaseConnection());
|
|
59
|
+
mockValidator.checkPrismaClientGenerated.mockReturnValue(defaultValidator.checkPrismaClientGenerated());
|
|
60
|
+
|
|
61
|
+
// Mock dotenv
|
|
62
|
+
dotenv.config = jest.fn();
|
|
63
|
+
|
|
28
64
|
// Clear environment variables
|
|
29
65
|
delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
|
|
30
66
|
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
@@ -40,32 +76,37 @@ describe('startCommand', () => {
|
|
|
40
76
|
});
|
|
41
77
|
|
|
42
78
|
afterEach(() => {
|
|
79
|
+
// Restore process.exit
|
|
80
|
+
if (mockProcessExit) {
|
|
81
|
+
mockProcessExit.mockRestore();
|
|
82
|
+
}
|
|
83
|
+
|
|
43
84
|
// Clean up environment
|
|
44
85
|
delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
|
|
45
86
|
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
46
87
|
});
|
|
47
88
|
|
|
48
|
-
it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', () => {
|
|
89
|
+
it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', async () => {
|
|
49
90
|
const options = { stage: 'dev' };
|
|
50
91
|
|
|
51
|
-
startCommand(options);
|
|
92
|
+
await startCommand(options);
|
|
52
93
|
|
|
53
94
|
// Verify the environment variable is set in the parent process
|
|
54
95
|
expect(process.env.FRIGG_SKIP_AWS_DISCOVERY).toBe('true');
|
|
55
96
|
});
|
|
56
97
|
|
|
57
|
-
it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', () => {
|
|
98
|
+
it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', async () => {
|
|
58
99
|
const options = { stage: 'dev' };
|
|
59
100
|
|
|
60
|
-
startCommand(options);
|
|
101
|
+
await startCommand(options);
|
|
61
102
|
|
|
62
103
|
expect(process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE).toBe('1');
|
|
63
104
|
});
|
|
64
105
|
|
|
65
|
-
it('should spawn serverless with correct arguments', () => {
|
|
106
|
+
it('should spawn serverless with correct arguments', async () => {
|
|
66
107
|
const options = { stage: 'prod' };
|
|
67
108
|
|
|
68
|
-
startCommand(options);
|
|
109
|
+
await startCommand(options);
|
|
69
110
|
|
|
70
111
|
expect(spawn).toHaveBeenCalledWith(
|
|
71
112
|
'serverless',
|
|
@@ -80,10 +121,10 @@ describe('startCommand', () => {
|
|
|
80
121
|
);
|
|
81
122
|
});
|
|
82
123
|
|
|
83
|
-
it('should include verbose flag when verbose option is enabled', () => {
|
|
124
|
+
it('should include verbose flag when verbose option is enabled', async () => {
|
|
84
125
|
const options = { stage: 'dev', verbose: true };
|
|
85
126
|
|
|
86
|
-
startCommand(options);
|
|
127
|
+
await startCommand(options);
|
|
87
128
|
|
|
88
129
|
expect(spawn).toHaveBeenCalledWith(
|
|
89
130
|
'serverless',
|
|
@@ -92,10 +133,10 @@ describe('startCommand', () => {
|
|
|
92
133
|
);
|
|
93
134
|
});
|
|
94
135
|
|
|
95
|
-
it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', () => {
|
|
136
|
+
it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', async () => {
|
|
96
137
|
const options = { stage: 'dev' };
|
|
97
138
|
|
|
98
|
-
startCommand(options);
|
|
139
|
+
await startCommand(options);
|
|
99
140
|
|
|
100
141
|
const spawnCall = spawn.mock.calls[0];
|
|
101
142
|
const spawnOptions = spawnCall[2];
|
|
@@ -103,11 +144,11 @@ describe('startCommand', () => {
|
|
|
103
144
|
expect(spawnOptions.env).toHaveProperty('FRIGG_SKIP_AWS_DISCOVERY', 'true');
|
|
104
145
|
});
|
|
105
146
|
|
|
106
|
-
it('should handle child process errors', () => {
|
|
147
|
+
it('should handle child process errors', async () => {
|
|
107
148
|
const options = { stage: 'dev' };
|
|
108
149
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
109
150
|
|
|
110
|
-
startCommand(options);
|
|
151
|
+
await startCommand(options);
|
|
111
152
|
|
|
112
153
|
// Simulate an error
|
|
113
154
|
const errorCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'error')[1];
|
|
@@ -119,11 +160,11 @@ describe('startCommand', () => {
|
|
|
119
160
|
consoleErrorSpy.mockRestore();
|
|
120
161
|
});
|
|
121
162
|
|
|
122
|
-
it('should handle child process exit with non-zero code', () => {
|
|
163
|
+
it('should handle child process exit with non-zero code', async () => {
|
|
123
164
|
const options = { stage: 'dev' };
|
|
124
165
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
125
166
|
|
|
126
|
-
startCommand(options);
|
|
167
|
+
await startCommand(options);
|
|
127
168
|
|
|
128
169
|
// Simulate exit with error code
|
|
129
170
|
const closeCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'close')[1];
|
|
@@ -134,11 +175,11 @@ describe('startCommand', () => {
|
|
|
134
175
|
consoleLogSpy.mockRestore();
|
|
135
176
|
});
|
|
136
177
|
|
|
137
|
-
it('should not log on successful exit', () => {
|
|
178
|
+
it('should not log on successful exit', async () => {
|
|
138
179
|
const options = { stage: 'dev' };
|
|
139
180
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
140
181
|
|
|
141
|
-
startCommand(options);
|
|
182
|
+
await startCommand(options);
|
|
142
183
|
|
|
143
184
|
// Clear the spy calls from startCommand execution
|
|
144
185
|
consoleLogSpy.mockClear();
|
|
@@ -152,4 +193,121 @@ describe('startCommand', () => {
|
|
|
152
193
|
|
|
153
194
|
consoleLogSpy.mockRestore();
|
|
154
195
|
});
|
|
196
|
+
|
|
197
|
+
describe('Database Pre-flight Validation', () => {
|
|
198
|
+
let mockConsoleError;
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
// Mock console.error (all other mocks are set up in outer beforeEach)
|
|
202
|
+
mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
mockConsoleError.mockRestore();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should pass pre-flight checks when database valid', async () => {
|
|
210
|
+
const options = { stage: 'dev' };
|
|
211
|
+
|
|
212
|
+
await startCommand(options);
|
|
213
|
+
|
|
214
|
+
expect(mockValidator.validateDatabaseUrl).toHaveBeenCalled();
|
|
215
|
+
expect(mockValidator.getDatabaseType).toHaveBeenCalled();
|
|
216
|
+
expect(mockValidator.testDatabaseConnection).toHaveBeenCalled();
|
|
217
|
+
expect(mockValidator.checkPrismaClientGenerated).toHaveBeenCalled();
|
|
218
|
+
expect(mockProcessExit).not.toHaveBeenCalled();
|
|
219
|
+
expect(spawn).toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should fail when DATABASE_URL missing', async () => {
|
|
223
|
+
mockValidator.validateDatabaseUrl.mockReturnValue({
|
|
224
|
+
valid: false,
|
|
225
|
+
error: 'DATABASE_URL not found'
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await expect(startCommand({})).rejects.toThrow('process.exit called');
|
|
229
|
+
|
|
230
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
231
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
|
232
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should fail when database type not configured', async () => {
|
|
236
|
+
mockValidator.getDatabaseType.mockReturnValue({
|
|
237
|
+
error: 'Database not configured'
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await expect(startCommand({})).rejects.toThrow('process.exit called');
|
|
241
|
+
|
|
242
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
243
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
|
244
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should fail when database connection fails', async () => {
|
|
248
|
+
mockValidator.testDatabaseConnection.mockResolvedValue({
|
|
249
|
+
connected: false,
|
|
250
|
+
error: 'Connection failed'
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await expect(startCommand({})).rejects.toThrow('process.exit called');
|
|
254
|
+
|
|
255
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
256
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
|
257
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should fail when Prisma client not generated', async () => {
|
|
261
|
+
mockValidator.checkPrismaClientGenerated.mockReturnValue({
|
|
262
|
+
generated: false,
|
|
263
|
+
error: 'Client not found'
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await expect(startCommand({})).rejects.toThrow('process.exit called');
|
|
267
|
+
|
|
268
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
269
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
|
|
270
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
|
271
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should suggest running frigg db:setup when client missing', async () => {
|
|
275
|
+
mockValidator.checkPrismaClientGenerated.mockReturnValue({
|
|
276
|
+
generated: false,
|
|
277
|
+
error: 'Client not generated'
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await expect(startCommand({})).rejects.toThrow('process.exit called');
|
|
281
|
+
|
|
282
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should exit with code 1 on validation failure', async () => {
|
|
286
|
+
mockValidator.validateDatabaseUrl.mockReturnValue({
|
|
287
|
+
valid: false
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await expect(startCommand({})).rejects.toThrow('process.exit called');
|
|
291
|
+
|
|
292
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should continue to serverless start when validation passes', async () => {
|
|
296
|
+
await startCommand({ stage: 'dev' });
|
|
297
|
+
|
|
298
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
299
|
+
'serverless',
|
|
300
|
+
expect.arrayContaining(['offline']),
|
|
301
|
+
expect.any(Object)
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should load .env before validation', async () => {
|
|
306
|
+
await startCommand({});
|
|
307
|
+
|
|
308
|
+
expect(dotenv.config).toHaveBeenCalledWith(expect.objectContaining({
|
|
309
|
+
path: expect.stringContaining('.env')
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
});
|
|
155
313
|
});
|