@friggframework/devtools 2.0.0--canary.546.74db90f.0 → 2.0.0--canary.545.e7becd9.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/README.md +1 -1
- package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
- package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
- package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
- package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
- package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
- package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
- package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
- package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
- package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
- package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
- package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
- package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
- package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
- package/frigg-cli/build-command/index.js +123 -11
- package/frigg-cli/container.js +172 -0
- package/frigg-cli/deploy-command/index.js +83 -1
- package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
- package/frigg-cli/doctor-command/index.js +37 -16
- package/frigg-cli/domain/entities/ApiModule.js +272 -0
- package/frigg-cli/domain/entities/AppDefinition.js +227 -0
- package/frigg-cli/domain/entities/Integration.js +198 -0
- package/frigg-cli/domain/exceptions/DomainException.js +24 -0
- package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
- package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
- package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
- package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
- package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
- package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
- package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
- package/frigg-cli/generate-iam-command.js +21 -1
- package/frigg-cli/index.js +21 -6
- package/frigg-cli/index.test.js +7 -2
- package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
- package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
- package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
- package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
- package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
- package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
- package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
- package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
- package/frigg-cli/init-command/backend-first-handler.js +124 -42
- package/frigg-cli/init-command/index.js +2 -1
- package/frigg-cli/init-command/template-handler.js +13 -3
- package/frigg-cli/install-command/backend-js.js +3 -3
- package/frigg-cli/install-command/environment-variables.js +16 -19
- package/frigg-cli/install-command/environment-variables.test.js +12 -13
- package/frigg-cli/install-command/index.js +14 -9
- package/frigg-cli/install-command/integration-file.js +3 -3
- package/frigg-cli/install-command/validate-package.js +5 -9
- package/frigg-cli/jest.config.js +4 -1
- package/frigg-cli/package-lock.json +16226 -0
- package/frigg-cli/repair-command/index.js +121 -128
- package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
- package/frigg-cli/start-command/index.js +324 -2
- package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
- package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
- package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
- package/frigg-cli/templates/backend/.env.example +62 -0
- package/frigg-cli/templates/backend/.eslintrc.json +12 -0
- package/frigg-cli/templates/backend/.prettierrc +6 -0
- package/frigg-cli/templates/backend/docker-compose.yml +22 -0
- package/frigg-cli/templates/backend/index.js +96 -0
- package/frigg-cli/templates/backend/infrastructure.js +12 -0
- package/frigg-cli/templates/backend/jest.config.js +17 -0
- package/frigg-cli/templates/backend/package.json +50 -0
- package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
- package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
- package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
- package/frigg-cli/templates/backend/test/setup.js +30 -0
- package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
- package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
- package/frigg-cli/ui-command/index.js +58 -36
- package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
- package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
- package/frigg-cli/utils/output.js +382 -0
- package/frigg-cli/utils/provider-helper.js +75 -0
- package/frigg-cli/utils/repo-detection.js +85 -37
- package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
- package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
- package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
- package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
- package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
- package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
- package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
- package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
- package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
- package/infrastructure/create-frigg-infrastructure.js +93 -0
- package/infrastructure/docs/iam-policy-templates.md +1 -1
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
- package/infrastructure/domains/admin-scripts/index.js +5 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
- package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +4 -7
- package/infrastructure/domains/shared/resource-discovery.js +5 -5
- package/infrastructure/domains/shared/types/app-definition.js +21 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
- package/infrastructure/infrastructure-composer.js +2 -0
- package/infrastructure/infrastructure-composer.test.js +2 -2
- package/infrastructure/jest.config.js +16 -0
- package/management-ui/README.md +245 -109
- package/package.json +8 -7
- package/frigg-cli/install-command/logger.js +0 -12
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const {
|
|
5
|
+
createValidateCommand,
|
|
6
|
+
formatConsoleOutput,
|
|
7
|
+
autoDetectFriggApp,
|
|
8
|
+
findBackendPathInDir,
|
|
9
|
+
findBackendPath
|
|
10
|
+
} = require('../../adapters/cli/validate-command');
|
|
11
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
12
|
+
const { ValidationError } = require('../../domain/value-objects/validation-error');
|
|
13
|
+
const { FixSuggestion } = require('../../domain/value-objects/fix-suggestion');
|
|
14
|
+
const { Command } = require('commander');
|
|
15
|
+
|
|
16
|
+
describe('validateCommand', () => {
|
|
17
|
+
let mockOutput;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockOutput = {
|
|
21
|
+
info: jest.fn(),
|
|
22
|
+
success: jest.fn(),
|
|
23
|
+
error: jest.fn(),
|
|
24
|
+
warn: jest.fn(),
|
|
25
|
+
log: jest.fn()
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('createValidateCommand', () => {
|
|
30
|
+
it('registers validate command on program', () => {
|
|
31
|
+
const program = new Command();
|
|
32
|
+
createValidateCommand(program);
|
|
33
|
+
const validateCmd = program.commands.find(c => c.name() === 'validate');
|
|
34
|
+
expect(validateCmd).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('has required options', () => {
|
|
38
|
+
const program = new Command();
|
|
39
|
+
createValidateCommand(program);
|
|
40
|
+
const validateCmd = program.commands.find(c => c.name() === 'validate');
|
|
41
|
+
const options = validateCmd.options.map(o => o.long);
|
|
42
|
+
expect(options).toContain('--format');
|
|
43
|
+
expect(options).toContain('--verbose');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('formatConsoleOutput', () => {
|
|
48
|
+
it('outputs success for valid result', () => {
|
|
49
|
+
const result = ValidationResult.create();
|
|
50
|
+
formatConsoleOutput(result, {}, mockOutput);
|
|
51
|
+
expect(mockOutput.success).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('outputs errors for invalid result', () => {
|
|
55
|
+
const error = ValidationError.create({
|
|
56
|
+
path: 'integrations',
|
|
57
|
+
message: 'Missing integrations',
|
|
58
|
+
severity: 'error'
|
|
59
|
+
});
|
|
60
|
+
const result = ValidationResult.create({ errors: [error] });
|
|
61
|
+
formatConsoleOutput(result, {}, mockOutput);
|
|
62
|
+
const logCalls = mockOutput.log.mock.calls.flat().join(' ');
|
|
63
|
+
expect(logCalls).toContain('Missing integrations');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('outputs warnings', () => {
|
|
67
|
+
const warning = ValidationError.create({
|
|
68
|
+
path: 'database',
|
|
69
|
+
message: 'No database configured',
|
|
70
|
+
severity: 'warning'
|
|
71
|
+
});
|
|
72
|
+
const result = ValidationResult.create({ errors: [warning] });
|
|
73
|
+
formatConsoleOutput(result, {}, mockOutput);
|
|
74
|
+
expect(mockOutput.warn).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('shows fix suggestions in verbose mode', () => {
|
|
78
|
+
const fix = FixSuggestion.create({ action: 'add', description: 'Add database configuration' });
|
|
79
|
+
const error = ValidationError.create({
|
|
80
|
+
path: 'database',
|
|
81
|
+
message: 'Missing config',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
fix
|
|
84
|
+
});
|
|
85
|
+
const result = ValidationResult.create({ errors: [error] });
|
|
86
|
+
formatConsoleOutput(result, { verbose: true }, mockOutput);
|
|
87
|
+
const logCalls = mockOutput.log.mock.calls.flat().join(' ');
|
|
88
|
+
expect(logCalls).toContain('Add database configuration');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('findBackendPathInDir', () => {
|
|
93
|
+
let tmpDir;
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-test-'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('finds backend/index.js', () => {
|
|
104
|
+
const backendDir = path.join(tmpDir, 'backend');
|
|
105
|
+
fs.mkdirSync(backendDir);
|
|
106
|
+
fs.writeFileSync(path.join(backendDir, 'index.js'), 'module.exports = {}');
|
|
107
|
+
expect(findBackendPathInDir(tmpDir)).toBe(backendDir);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('finds index.js with integrations keyword', () => {
|
|
111
|
+
fs.writeFileSync(path.join(tmpDir, 'index.js'), 'module.exports = { integrations: [] }');
|
|
112
|
+
expect(findBackendPathInDir(tmpDir)).toBe(tmpDir);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('finds index.js with Definition keyword', () => {
|
|
116
|
+
fs.writeFileSync(path.join(tmpDir, 'index.js'), 'module.exports = { Definition: {} }');
|
|
117
|
+
expect(findBackendPathInDir(tmpDir)).toBe(tmpDir);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns null for non-frigg directory', () => {
|
|
121
|
+
fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello")');
|
|
122
|
+
expect(findBackendPathInDir(tmpDir)).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns null for empty directory', () => {
|
|
126
|
+
expect(findBackendPathInDir(tmpDir)).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('findBackendPath', () => {
|
|
131
|
+
let tmpDir;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-test-'));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('finds backend subdirectory with package.json', () => {
|
|
142
|
+
const backendDir = path.join(tmpDir, 'backend');
|
|
143
|
+
fs.mkdirSync(backendDir);
|
|
144
|
+
fs.writeFileSync(path.join(backendDir, 'package.json'), '{}');
|
|
145
|
+
expect(findBackendPath(tmpDir)).toBe(backendDir);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('finds backend subdirectory with index.js', () => {
|
|
149
|
+
const backendDir = path.join(tmpDir, 'backend');
|
|
150
|
+
fs.mkdirSync(backendDir);
|
|
151
|
+
fs.writeFileSync(path.join(backendDir, 'index.js'), 'module.exports = {}');
|
|
152
|
+
expect(findBackendPath(tmpDir)).toBe(backendDir);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns path itself if it has package.json', () => {
|
|
156
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
157
|
+
expect(findBackendPath(tmpDir)).toBe(tmpDir);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns null for non-backend directory', () => {
|
|
161
|
+
expect(findBackendPath(tmpDir)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('autoDetectFriggApp', () => {
|
|
166
|
+
let tmpDir;
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-test-'));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('detects frigg app in current directory', () => {
|
|
177
|
+
const backendDir = path.join(tmpDir, 'backend');
|
|
178
|
+
fs.mkdirSync(backendDir);
|
|
179
|
+
fs.writeFileSync(path.join(backendDir, 'index.js'), 'module.exports = {}');
|
|
180
|
+
const result = autoDetectFriggApp(tmpDir);
|
|
181
|
+
expect(result).toEqual({
|
|
182
|
+
appRoot: tmpDir,
|
|
183
|
+
backendPath: backendDir
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('detects frigg app in parent directory', () => {
|
|
188
|
+
const backendDir = path.join(tmpDir, 'backend');
|
|
189
|
+
const subDir = path.join(tmpDir, 'src', 'components');
|
|
190
|
+
fs.mkdirSync(backendDir);
|
|
191
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
192
|
+
fs.writeFileSync(path.join(backendDir, 'index.js'), 'module.exports = {}');
|
|
193
|
+
const result = autoDetectFriggApp(subDir);
|
|
194
|
+
expect(result).toEqual({
|
|
195
|
+
appRoot: tmpDir,
|
|
196
|
+
backendPath: backendDir
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns null when no frigg app found', () => {
|
|
201
|
+
const result = autoDetectFriggApp(tmpDir);
|
|
202
|
+
expect(result).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const { ValidateAppUseCase } = require('../../application/use-cases/validate-app-use-case');
|
|
2
|
+
|
|
3
|
+
describe('ValidateAppUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockAppDefinitionValidator;
|
|
6
|
+
let mockIntegrationClassValidator;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockAppDefinitionValidator = {
|
|
10
|
+
validate: jest.fn()
|
|
11
|
+
};
|
|
12
|
+
mockIntegrationClassValidator = {
|
|
13
|
+
validate: jest.fn()
|
|
14
|
+
};
|
|
15
|
+
useCase = new ValidateAppUseCase({
|
|
16
|
+
appDefinitionValidator: mockAppDefinitionValidator,
|
|
17
|
+
integrationClassValidator: mockIntegrationClassValidator
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('execute', () => {
|
|
22
|
+
it('validates app definition structure', async () => {
|
|
23
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
24
|
+
mockAppDefinitionValidator.validate.mockReturnValue(ValidationResult.create());
|
|
25
|
+
mockIntegrationClassValidator.validate.mockReturnValue(ValidationResult.create());
|
|
26
|
+
|
|
27
|
+
const definition = { integrations: [] };
|
|
28
|
+
await useCase.execute({ definition });
|
|
29
|
+
|
|
30
|
+
expect(mockAppDefinitionValidator.validate).toHaveBeenCalledWith(definition);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('validates each integration class', async () => {
|
|
34
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
35
|
+
mockAppDefinitionValidator.validate.mockReturnValue(ValidationResult.create());
|
|
36
|
+
mockIntegrationClassValidator.validate.mockReturnValue(ValidationResult.create());
|
|
37
|
+
|
|
38
|
+
class Int1 { static Definition = { name: 'int1' }; }
|
|
39
|
+
class Int2 { static Definition = { name: 'int2' }; }
|
|
40
|
+
const definition = { integrations: [Int1, Int2] };
|
|
41
|
+
|
|
42
|
+
await useCase.execute({ definition });
|
|
43
|
+
|
|
44
|
+
expect(mockIntegrationClassValidator.validate).toHaveBeenCalledWith(Int1, 0);
|
|
45
|
+
expect(mockIntegrationClassValidator.validate).toHaveBeenCalledWith(Int2, 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('merges all validation results', async () => {
|
|
49
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
50
|
+
const { ValidationError } = require('../../domain/value-objects/validation-error');
|
|
51
|
+
|
|
52
|
+
const appError = ValidationError.create({ path: 'database', message: 'missing', severity: 'error' });
|
|
53
|
+
const intError = ValidationError.create({ path: 'integrations[0]', message: 'invalid', severity: 'error' });
|
|
54
|
+
|
|
55
|
+
mockAppDefinitionValidator.validate.mockReturnValue(
|
|
56
|
+
ValidationResult.create({ errors: [appError] })
|
|
57
|
+
);
|
|
58
|
+
mockIntegrationClassValidator.validate.mockReturnValue(
|
|
59
|
+
ValidationResult.create({ errors: [intError] })
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
class Int1 { static Definition = { name: 'int1' }; }
|
|
63
|
+
const definition = { integrations: [Int1] };
|
|
64
|
+
|
|
65
|
+
const result = await useCase.execute({ definition });
|
|
66
|
+
|
|
67
|
+
expect(result.getErrors()).toHaveLength(2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns valid result when no errors', async () => {
|
|
71
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
72
|
+
mockAppDefinitionValidator.validate.mockReturnValue(ValidationResult.create());
|
|
73
|
+
mockIntegrationClassValidator.validate.mockReturnValue(ValidationResult.create());
|
|
74
|
+
|
|
75
|
+
const definition = { integrations: [] };
|
|
76
|
+
const result = await useCase.execute({ definition });
|
|
77
|
+
|
|
78
|
+
expect(result.isValid()).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('adds context with definition metadata', async () => {
|
|
82
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
83
|
+
mockAppDefinitionValidator.validate.mockReturnValue(ValidationResult.create());
|
|
84
|
+
|
|
85
|
+
const definition = { integrations: [] };
|
|
86
|
+
const result = await useCase.execute({ definition, appPath: '/app/backend' });
|
|
87
|
+
|
|
88
|
+
expect(result.getContext()).toMatchObject({ appPath: '/app/backend' });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('error handling', () => {
|
|
93
|
+
it('handles validator throwing error', async () => {
|
|
94
|
+
mockAppDefinitionValidator.validate.mockImplementation(() => {
|
|
95
|
+
throw new Error('Validator crashed');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const definition = { integrations: [] };
|
|
99
|
+
|
|
100
|
+
await expect(useCase.execute({ definition }))
|
|
101
|
+
.rejects.toThrow('Validator crashed');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const { FixSuggestion } = require('../../domain/value-objects/fix-suggestion');
|
|
2
|
+
|
|
3
|
+
describe('FixSuggestion', () => {
|
|
4
|
+
describe('creation', () => {
|
|
5
|
+
it('creates fix with action and description', () => {
|
|
6
|
+
const fix = FixSuggestion.create({
|
|
7
|
+
action: 'add',
|
|
8
|
+
description: 'Add the name field to the configuration'
|
|
9
|
+
});
|
|
10
|
+
expect(fix.action).toBe('add');
|
|
11
|
+
expect(fix.description).toBe('Add the name field to the configuration');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('creates fix with template', () => {
|
|
15
|
+
const fix = FixSuggestion.create({
|
|
16
|
+
action: 'add',
|
|
17
|
+
description: 'Add database configuration',
|
|
18
|
+
template: { database: { mongoDB: { enable: true } } }
|
|
19
|
+
});
|
|
20
|
+
expect(fix.template).toEqual({ database: { mongoDB: { enable: true } } });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('creates fix with code snippet', () => {
|
|
24
|
+
const fix = FixSuggestion.create({
|
|
25
|
+
action: 'replace',
|
|
26
|
+
description: 'Update the export',
|
|
27
|
+
codeSnippet: 'module.exports = { Definition };'
|
|
28
|
+
});
|
|
29
|
+
expect(fix.codeSnippet).toBe('module.exports = { Definition };');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('throws for missing action', () => {
|
|
33
|
+
expect(() => FixSuggestion.create({ description: 'Fix it' }))
|
|
34
|
+
.toThrow('action is required');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws for missing description', () => {
|
|
38
|
+
expect(() => FixSuggestion.create({ action: 'add' }))
|
|
39
|
+
.toThrow('description is required');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws for invalid action', () => {
|
|
43
|
+
expect(() => FixSuggestion.create({ action: 'destroy', description: 'd' }))
|
|
44
|
+
.toThrow('Invalid action');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('actions', () => {
|
|
49
|
+
it('supports add action', () => {
|
|
50
|
+
const fix = FixSuggestion.create({ action: 'add', description: 'd' });
|
|
51
|
+
expect(fix.isAdd()).toBe(true);
|
|
52
|
+
expect(fix.isRemove()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('supports remove action', () => {
|
|
56
|
+
const fix = FixSuggestion.create({ action: 'remove', description: 'd' });
|
|
57
|
+
expect(fix.isRemove()).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('supports replace action', () => {
|
|
61
|
+
const fix = FixSuggestion.create({ action: 'replace', description: 'd' });
|
|
62
|
+
expect(fix.isReplace()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('supports rename action', () => {
|
|
66
|
+
const fix = FixSuggestion.create({ action: 'rename', description: 'd' });
|
|
67
|
+
expect(fix.isRename()).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('supports update action', () => {
|
|
71
|
+
const fix = FixSuggestion.create({ action: 'update', description: 'd' });
|
|
72
|
+
expect(fix.isUpdate()).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('applicability', () => {
|
|
77
|
+
it('is auto-applicable with template', () => {
|
|
78
|
+
const fix = FixSuggestion.create({
|
|
79
|
+
action: 'add',
|
|
80
|
+
description: 'd',
|
|
81
|
+
template: { key: 'value' }
|
|
82
|
+
});
|
|
83
|
+
expect(fix.isAutoApplicable()).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('is auto-applicable with code snippet', () => {
|
|
87
|
+
const fix = FixSuggestion.create({
|
|
88
|
+
action: 'replace',
|
|
89
|
+
description: 'd',
|
|
90
|
+
codeSnippet: 'const x = 1;'
|
|
91
|
+
});
|
|
92
|
+
expect(fix.isAutoApplicable()).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('is not auto-applicable without template or snippet', () => {
|
|
96
|
+
const fix = FixSuggestion.create({ action: 'add', description: 'd' });
|
|
97
|
+
expect(fix.isAutoApplicable()).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('target', () => {
|
|
102
|
+
it('sets target path', () => {
|
|
103
|
+
const fix = FixSuggestion.create({
|
|
104
|
+
action: 'add',
|
|
105
|
+
description: 'd',
|
|
106
|
+
targetPath: 'config.database'
|
|
107
|
+
});
|
|
108
|
+
expect(fix.targetPath).toBe('config.database');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('sets target file', () => {
|
|
112
|
+
const fix = FixSuggestion.create({
|
|
113
|
+
action: 'update',
|
|
114
|
+
description: 'd',
|
|
115
|
+
targetFile: 'backend/index.js'
|
|
116
|
+
});
|
|
117
|
+
expect(fix.targetFile).toBe('backend/index.js');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('serialization', () => {
|
|
122
|
+
it('converts to JSON', () => {
|
|
123
|
+
const fix = FixSuggestion.create({
|
|
124
|
+
action: 'add',
|
|
125
|
+
description: 'Add config',
|
|
126
|
+
template: { key: 'value' },
|
|
127
|
+
targetPath: 'config'
|
|
128
|
+
});
|
|
129
|
+
const json = fix.toJSON();
|
|
130
|
+
expect(json).toEqual({
|
|
131
|
+
action: 'add',
|
|
132
|
+
description: 'Add config',
|
|
133
|
+
template: { key: 'value' },
|
|
134
|
+
codeSnippet: null,
|
|
135
|
+
targetPath: 'config',
|
|
136
|
+
targetFile: null
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('formatting', () => {
|
|
142
|
+
it('formats for console display', () => {
|
|
143
|
+
const fix = FixSuggestion.create({
|
|
144
|
+
action: 'add',
|
|
145
|
+
description: 'Add the name field',
|
|
146
|
+
template: { name: 'my-app' }
|
|
147
|
+
});
|
|
148
|
+
const formatted = fix.format();
|
|
149
|
+
expect(formatted).toContain('add');
|
|
150
|
+
expect(formatted).toContain('Add the name field');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { ValidationError } = require('../../domain/value-objects/validation-error');
|
|
2
|
+
const { FixSuggestion } = require('../../domain/value-objects/fix-suggestion');
|
|
3
|
+
|
|
4
|
+
describe('ValidationError', () => {
|
|
5
|
+
describe('creation', () => {
|
|
6
|
+
it('creates error with required fields', () => {
|
|
7
|
+
const error = ValidationError.create({
|
|
8
|
+
path: 'integrations[0].name',
|
|
9
|
+
message: 'Integration name is required'
|
|
10
|
+
});
|
|
11
|
+
expect(error.path).toBe('integrations[0].name');
|
|
12
|
+
expect(error.message).toBe('Integration name is required');
|
|
13
|
+
expect(error.severity).toBe('error');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('creates error with custom severity', () => {
|
|
17
|
+
const warning = ValidationError.create({
|
|
18
|
+
path: 'config.timeout',
|
|
19
|
+
message: 'Timeout value seems high',
|
|
20
|
+
severity: 'warning'
|
|
21
|
+
});
|
|
22
|
+
expect(warning.severity).toBe('warning');
|
|
23
|
+
expect(warning.isWarning()).toBe(true);
|
|
24
|
+
expect(warning.isError()).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('creates error with fix suggestion', () => {
|
|
28
|
+
const fix = FixSuggestion.create({
|
|
29
|
+
action: 'add',
|
|
30
|
+
description: 'Add the name field to integration'
|
|
31
|
+
});
|
|
32
|
+
const error = ValidationError.create({
|
|
33
|
+
path: 'integrations[0].name',
|
|
34
|
+
message: 'Required field missing',
|
|
35
|
+
fix
|
|
36
|
+
});
|
|
37
|
+
expect(error.fix).toBe(fix);
|
|
38
|
+
expect(error.hasFix()).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws for missing path', () => {
|
|
42
|
+
expect(() => ValidationError.create({ message: 'error' }))
|
|
43
|
+
.toThrow('path is required');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('throws for missing message', () => {
|
|
47
|
+
expect(() => ValidationError.create({ path: 'a.b' }))
|
|
48
|
+
.toThrow('message is required');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('throws for invalid severity', () => {
|
|
52
|
+
expect(() => ValidationError.create({
|
|
53
|
+
path: 'a',
|
|
54
|
+
message: 'b',
|
|
55
|
+
severity: 'critical'
|
|
56
|
+
})).toThrow('Invalid severity');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('path parsing', () => {
|
|
61
|
+
it('parses simple path', () => {
|
|
62
|
+
const error = ValidationError.create({ path: 'name', message: 'm' });
|
|
63
|
+
expect(error.getPathSegments()).toEqual(['name']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('parses nested path', () => {
|
|
67
|
+
const error = ValidationError.create({ path: 'config.database.uri', message: 'm' });
|
|
68
|
+
expect(error.getPathSegments()).toEqual(['config', 'database', 'uri']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('parses array path', () => {
|
|
72
|
+
const error = ValidationError.create({ path: 'integrations[0].modules[1].name', message: 'm' });
|
|
73
|
+
expect(error.getPathSegments()).toEqual(['integrations', '0', 'modules', '1', 'name']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('gets root path', () => {
|
|
77
|
+
const error = ValidationError.create({ path: 'integrations[0].config.timeout', message: 'm' });
|
|
78
|
+
expect(error.getRootPath()).toBe('integrations');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('severity helpers', () => {
|
|
83
|
+
it('identifies error severity', () => {
|
|
84
|
+
const error = ValidationError.create({ path: 'a', message: 'm', severity: 'error' });
|
|
85
|
+
expect(error.isError()).toBe(true);
|
|
86
|
+
expect(error.isWarning()).toBe(false);
|
|
87
|
+
expect(error.isInfo()).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('identifies warning severity', () => {
|
|
91
|
+
const error = ValidationError.create({ path: 'a', message: 'm', severity: 'warning' });
|
|
92
|
+
expect(error.isError()).toBe(false);
|
|
93
|
+
expect(error.isWarning()).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('identifies info severity', () => {
|
|
97
|
+
const error = ValidationError.create({ path: 'a', message: 'm', severity: 'info' });
|
|
98
|
+
expect(error.isError()).toBe(false);
|
|
99
|
+
expect(error.isInfo()).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('code', () => {
|
|
104
|
+
it('assigns error code', () => {
|
|
105
|
+
const error = ValidationError.create({
|
|
106
|
+
path: 'name',
|
|
107
|
+
message: 'Required',
|
|
108
|
+
code: 'REQUIRED_FIELD'
|
|
109
|
+
});
|
|
110
|
+
expect(error.code).toBe('REQUIRED_FIELD');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('defaults to null code', () => {
|
|
114
|
+
const error = ValidationError.create({ path: 'a', message: 'm' });
|
|
115
|
+
expect(error.code).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('serialization', () => {
|
|
120
|
+
it('converts to JSON', () => {
|
|
121
|
+
const error = ValidationError.create({
|
|
122
|
+
path: 'config.name',
|
|
123
|
+
message: 'Name required',
|
|
124
|
+
severity: 'error',
|
|
125
|
+
code: 'REQUIRED'
|
|
126
|
+
});
|
|
127
|
+
const json = error.toJSON();
|
|
128
|
+
expect(json).toEqual({
|
|
129
|
+
path: 'config.name',
|
|
130
|
+
message: 'Name required',
|
|
131
|
+
severity: 'error',
|
|
132
|
+
code: 'REQUIRED',
|
|
133
|
+
fix: null
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('includes fix in JSON', () => {
|
|
138
|
+
const fix = FixSuggestion.create({ action: 'add', description: 'Add field' });
|
|
139
|
+
const error = ValidationError.create({
|
|
140
|
+
path: 'a',
|
|
141
|
+
message: 'm',
|
|
142
|
+
fix
|
|
143
|
+
});
|
|
144
|
+
const json = error.toJSON();
|
|
145
|
+
expect(json.fix).toMatchObject({ action: 'add', description: 'Add field' });
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('equality', () => {
|
|
150
|
+
it('considers errors equal by path and message', () => {
|
|
151
|
+
const error1 = ValidationError.create({ path: 'a.b', message: 'Required' });
|
|
152
|
+
const error2 = ValidationError.create({ path: 'a.b', message: 'Required' });
|
|
153
|
+
expect(error1.equals(error2)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('considers errors different by path', () => {
|
|
157
|
+
const error1 = ValidationError.create({ path: 'a.b', message: 'Required' });
|
|
158
|
+
const error2 = ValidationError.create({ path: 'a.c', message: 'Required' });
|
|
159
|
+
expect(error1.equals(error2)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|