@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,152 @@
|
|
|
1
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
2
|
+
const { ValidationError } = require('../../domain/value-objects/validation-error');
|
|
3
|
+
const { FixSuggestion } = require('../../domain/value-objects/fix-suggestion');
|
|
4
|
+
|
|
5
|
+
describe('ValidationResult', () => {
|
|
6
|
+
describe('creation', () => {
|
|
7
|
+
it('creates empty result with no errors', () => {
|
|
8
|
+
const result = ValidationResult.create();
|
|
9
|
+
expect(result.isValid()).toBe(true);
|
|
10
|
+
expect(result.getErrors()).toHaveLength(0);
|
|
11
|
+
expect(result.getWarnings()).toHaveLength(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('creates result with errors', () => {
|
|
15
|
+
const error = ValidationError.create({
|
|
16
|
+
path: 'integrations[0].name',
|
|
17
|
+
message: 'Integration name is required',
|
|
18
|
+
severity: 'error'
|
|
19
|
+
});
|
|
20
|
+
const result = ValidationResult.create({ errors: [error] });
|
|
21
|
+
expect(result.isValid()).toBe(false);
|
|
22
|
+
expect(result.getErrors()).toHaveLength(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('creates result with warnings', () => {
|
|
26
|
+
const warning = ValidationError.create({
|
|
27
|
+
path: 'config.timeout',
|
|
28
|
+
message: 'Timeout value is unusually high',
|
|
29
|
+
severity: 'warning'
|
|
30
|
+
});
|
|
31
|
+
const result = ValidationResult.create({ errors: [warning] });
|
|
32
|
+
expect(result.isValid()).toBe(true);
|
|
33
|
+
expect(result.getWarnings()).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('addError', () => {
|
|
38
|
+
it('adds error to result', () => {
|
|
39
|
+
const result = ValidationResult.create();
|
|
40
|
+
const error = ValidationError.create({
|
|
41
|
+
path: 'name',
|
|
42
|
+
message: 'Name is required',
|
|
43
|
+
severity: 'error'
|
|
44
|
+
});
|
|
45
|
+
result.addError(error);
|
|
46
|
+
expect(result.isValid()).toBe(false);
|
|
47
|
+
expect(result.getErrors()).toContain(error);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('adds warning without affecting validity', () => {
|
|
51
|
+
const result = ValidationResult.create();
|
|
52
|
+
const warning = ValidationError.create({
|
|
53
|
+
path: 'description',
|
|
54
|
+
message: 'Description is recommended',
|
|
55
|
+
severity: 'warning'
|
|
56
|
+
});
|
|
57
|
+
result.addError(warning);
|
|
58
|
+
expect(result.isValid()).toBe(true);
|
|
59
|
+
expect(result.getWarnings()).toContain(warning);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('merge', () => {
|
|
64
|
+
it('merges two valid results', () => {
|
|
65
|
+
const result1 = ValidationResult.create();
|
|
66
|
+
const result2 = ValidationResult.create();
|
|
67
|
+
const merged = result1.merge(result2);
|
|
68
|
+
expect(merged.isValid()).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('merges results with errors', () => {
|
|
72
|
+
const error1 = ValidationError.create({
|
|
73
|
+
path: 'name',
|
|
74
|
+
message: 'Name required',
|
|
75
|
+
severity: 'error'
|
|
76
|
+
});
|
|
77
|
+
const error2 = ValidationError.create({
|
|
78
|
+
path: 'version',
|
|
79
|
+
message: 'Version required',
|
|
80
|
+
severity: 'error'
|
|
81
|
+
});
|
|
82
|
+
const result1 = ValidationResult.create({ errors: [error1] });
|
|
83
|
+
const result2 = ValidationResult.create({ errors: [error2] });
|
|
84
|
+
const merged = result1.merge(result2);
|
|
85
|
+
expect(merged.isValid()).toBe(false);
|
|
86
|
+
expect(merged.getErrors()).toHaveLength(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('preserves context from both results', () => {
|
|
90
|
+
const result1 = ValidationResult.create({ context: { file: 'index.js' } });
|
|
91
|
+
const result2 = ValidationResult.create({ context: { integration: 'oauth' } });
|
|
92
|
+
const merged = result1.merge(result2);
|
|
93
|
+
expect(merged.getContext()).toMatchObject({ file: 'index.js', integration: 'oauth' });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('filtering', () => {
|
|
98
|
+
it('filters errors by path prefix', () => {
|
|
99
|
+
const errors = [
|
|
100
|
+
ValidationError.create({ path: 'integrations[0].name', message: 'a', severity: 'error' }),
|
|
101
|
+
ValidationError.create({ path: 'integrations[1].config', message: 'b', severity: 'error' }),
|
|
102
|
+
ValidationError.create({ path: 'database.uri', message: 'c', severity: 'error' })
|
|
103
|
+
];
|
|
104
|
+
const result = ValidationResult.create({ errors });
|
|
105
|
+
const filtered = result.filterByPath('integrations');
|
|
106
|
+
expect(filtered.getErrors()).toHaveLength(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('filters by severity', () => {
|
|
110
|
+
const errors = [
|
|
111
|
+
ValidationError.create({ path: 'a', message: 'error1', severity: 'error' }),
|
|
112
|
+
ValidationError.create({ path: 'b', message: 'warning1', severity: 'warning' }),
|
|
113
|
+
ValidationError.create({ path: 'c', message: 'info1', severity: 'info' })
|
|
114
|
+
];
|
|
115
|
+
const result = ValidationResult.create({ errors });
|
|
116
|
+
expect(result.getBySeverity('error')).toHaveLength(1);
|
|
117
|
+
expect(result.getBySeverity('warning')).toHaveLength(1);
|
|
118
|
+
expect(result.getBySeverity('info')).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('summary', () => {
|
|
123
|
+
it('generates summary statistics', () => {
|
|
124
|
+
const errors = [
|
|
125
|
+
ValidationError.create({ path: 'a', message: 'm1', severity: 'error' }),
|
|
126
|
+
ValidationError.create({ path: 'b', message: 'm2', severity: 'error' }),
|
|
127
|
+
ValidationError.create({ path: 'c', message: 'm3', severity: 'warning' })
|
|
128
|
+
];
|
|
129
|
+
const result = ValidationResult.create({ errors });
|
|
130
|
+
const summary = result.getSummary();
|
|
131
|
+
expect(summary.errorCount).toBe(2);
|
|
132
|
+
expect(summary.warningCount).toBe(1);
|
|
133
|
+
expect(summary.isValid).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('serialization', () => {
|
|
138
|
+
it('converts to JSON', () => {
|
|
139
|
+
const error = ValidationError.create({
|
|
140
|
+
path: 'name',
|
|
141
|
+
message: 'Required',
|
|
142
|
+
severity: 'error',
|
|
143
|
+
fix: FixSuggestion.create({ action: 'add', description: 'Add name field' })
|
|
144
|
+
});
|
|
145
|
+
const result = ValidationResult.create({ errors: [error] });
|
|
146
|
+
const json = result.toJSON();
|
|
147
|
+
expect(json).toHaveProperty('valid', false);
|
|
148
|
+
expect(json).toHaveProperty('errors');
|
|
149
|
+
expect(json.errors[0]).toHaveProperty('path', 'name');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
const { ApiModuleValidator } = require('../../infrastructure/validators/api-module-validator');
|
|
2
|
+
|
|
3
|
+
describe('ApiModuleValidator', () => {
|
|
4
|
+
let validator;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
validator = new ApiModuleValidator();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('valid modules', () => {
|
|
11
|
+
it('validates integration with no modules', () => {
|
|
12
|
+
const definition = {
|
|
13
|
+
name: 'test-integration',
|
|
14
|
+
version: '1.0.0'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const result = validator.validate(definition, 0);
|
|
18
|
+
expect(result.isValid()).toBe(true);
|
|
19
|
+
expect(result.getErrors()).toHaveLength(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('validates integration with valid module definition', () => {
|
|
23
|
+
const definition = {
|
|
24
|
+
name: 'test-integration',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
modules: {
|
|
27
|
+
hubspot: {
|
|
28
|
+
definition: {
|
|
29
|
+
moduleName: 'hubspot',
|
|
30
|
+
getName: { type: 'function' },
|
|
31
|
+
requiredAuthMethods: {
|
|
32
|
+
getToken: { type: 'function', async: true },
|
|
33
|
+
getCredentialDetails: { type: 'function', async: true },
|
|
34
|
+
getEntityDetails: { type: 'function', async: true },
|
|
35
|
+
apiPropertiesToPersist: {
|
|
36
|
+
credential: ['access_token', 'refresh_token'],
|
|
37
|
+
entity: ['external_id', 'name']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = validator.validate(definition, 0);
|
|
46
|
+
expect(result.isValid()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('validates module with actual functions (not descriptors)', () => {
|
|
50
|
+
// Real-world API modules have actual functions, not {type: "function"} descriptors
|
|
51
|
+
// The validator should sanitize these before JSON Schema validation
|
|
52
|
+
const definition = {
|
|
53
|
+
name: 'test-integration',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
modules: {
|
|
56
|
+
xero: {
|
|
57
|
+
definition: {
|
|
58
|
+
moduleName: 'xero',
|
|
59
|
+
getName: function() { return 'Xero'; },
|
|
60
|
+
API: class XeroApi {},
|
|
61
|
+
requiredAuthMethods: {
|
|
62
|
+
getToken: async function() { return {}; },
|
|
63
|
+
getCredentialDetails: async function() { return {}; },
|
|
64
|
+
getEntityDetails: async function() { return {}; },
|
|
65
|
+
apiPropertiesToPersist: {
|
|
66
|
+
credential: ['access_token', 'refresh_token'],
|
|
67
|
+
entity: ['tenant_id', 'name']
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const result = validator.validate(definition, 0);
|
|
76
|
+
// Should not have errors about "must be object" for functions
|
|
77
|
+
const typeErrors = result.getErrors().filter(e =>
|
|
78
|
+
e.message.includes('must be object') ||
|
|
79
|
+
e.message.includes('must be Object')
|
|
80
|
+
);
|
|
81
|
+
expect(typeErrors).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
it('validates multiple modules', () => {
|
|
86
|
+
const definition = {
|
|
87
|
+
name: 'test-integration',
|
|
88
|
+
version: '1.0.0',
|
|
89
|
+
modules: {
|
|
90
|
+
source: {
|
|
91
|
+
definition: {
|
|
92
|
+
moduleName: 'source-api',
|
|
93
|
+
getName: { type: 'function' },
|
|
94
|
+
requiredAuthMethods: {
|
|
95
|
+
getToken: { type: 'function', async: true },
|
|
96
|
+
getCredentialDetails: { type: 'function', async: true },
|
|
97
|
+
getEntityDetails: { type: 'function', async: true },
|
|
98
|
+
apiPropertiesToPersist: {
|
|
99
|
+
credential: ['token'],
|
|
100
|
+
entity: ['id']
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
target: {
|
|
106
|
+
definition: {
|
|
107
|
+
moduleName: 'target-api',
|
|
108
|
+
getName: { type: 'function' },
|
|
109
|
+
requiredAuthMethods: {
|
|
110
|
+
getToken: { type: 'function', async: true },
|
|
111
|
+
getCredentialDetails: { type: 'function', async: true },
|
|
112
|
+
getEntityDetails: { type: 'function', async: true },
|
|
113
|
+
apiPropertiesToPersist: {
|
|
114
|
+
credential: ['api_key'],
|
|
115
|
+
entity: ['external_id']
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = validator.validate(definition, 0);
|
|
124
|
+
expect(result.isValid()).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('missing definition', () => {
|
|
129
|
+
it('errors when module lacks definition property', () => {
|
|
130
|
+
const definition = {
|
|
131
|
+
name: 'test-integration',
|
|
132
|
+
version: '1.0.0',
|
|
133
|
+
modules: {
|
|
134
|
+
hubspot: {
|
|
135
|
+
// missing definition
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = validator.validate(definition, 0);
|
|
141
|
+
expect(result.isValid()).toBe(false);
|
|
142
|
+
expect(result.getErrors().some(e => e.code === 'MISSING_DEFINITION')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('schema validation', () => {
|
|
147
|
+
it('errors when moduleName is missing', () => {
|
|
148
|
+
const definition = {
|
|
149
|
+
name: 'test-integration',
|
|
150
|
+
version: '1.0.0',
|
|
151
|
+
modules: {
|
|
152
|
+
hubspot: {
|
|
153
|
+
definition: {
|
|
154
|
+
// missing moduleName
|
|
155
|
+
getName: { type: 'function' },
|
|
156
|
+
requiredAuthMethods: {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = validator.validate(definition, 0);
|
|
163
|
+
expect(result.isValid()).toBe(false);
|
|
164
|
+
expect(result.getErrors().some(e =>
|
|
165
|
+
e.path.includes('definition') && e.message.includes('moduleName')
|
|
166
|
+
)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('errors when moduleName has invalid pattern', () => {
|
|
170
|
+
const definition = {
|
|
171
|
+
name: 'test-integration',
|
|
172
|
+
version: '1.0.0',
|
|
173
|
+
modules: {
|
|
174
|
+
test: {
|
|
175
|
+
definition: {
|
|
176
|
+
moduleName: '123-invalid', // must start with letter
|
|
177
|
+
getName: { type: 'function' },
|
|
178
|
+
requiredAuthMethods: {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = validator.validate(definition, 0);
|
|
185
|
+
expect(result.isValid()).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('method validation', () => {
|
|
190
|
+
it('warns when getToken is missing', () => {
|
|
191
|
+
const definition = {
|
|
192
|
+
name: 'test-integration',
|
|
193
|
+
version: '1.0.0',
|
|
194
|
+
modules: {
|
|
195
|
+
test: {
|
|
196
|
+
definition: {
|
|
197
|
+
moduleName: 'test-module',
|
|
198
|
+
getName: { type: 'function' },
|
|
199
|
+
requiredAuthMethods: {
|
|
200
|
+
getCredentialDetails: { type: 'function' },
|
|
201
|
+
getEntityDetails: { type: 'function' }
|
|
202
|
+
// missing getToken
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const result = validator.validate(definition, 0);
|
|
210
|
+
const warnings = result.getWarnings();
|
|
211
|
+
expect(warnings.some(w =>
|
|
212
|
+
w.code === 'MISSING_METHOD' && w.message.includes('getToken')
|
|
213
|
+
)).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('warns when getEntityDetails is missing', () => {
|
|
217
|
+
const definition = {
|
|
218
|
+
name: 'test-integration',
|
|
219
|
+
version: '1.0.0',
|
|
220
|
+
modules: {
|
|
221
|
+
test: {
|
|
222
|
+
definition: {
|
|
223
|
+
moduleName: 'test-module',
|
|
224
|
+
getName: { type: 'function' },
|
|
225
|
+
requiredAuthMethods: {
|
|
226
|
+
getToken: { type: 'function' },
|
|
227
|
+
getCredentialDetails: { type: 'function' }
|
|
228
|
+
// missing getEntityDetails
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const result = validator.validate(definition, 0);
|
|
236
|
+
const warnings = result.getWarnings();
|
|
237
|
+
expect(warnings.some(w =>
|
|
238
|
+
w.code === 'MISSING_METHOD' && w.message.includes('getEntityDetails')
|
|
239
|
+
)).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('apiPropertiesToPersist validation', () => {
|
|
244
|
+
it('warns when credential properties are missing', () => {
|
|
245
|
+
const definition = {
|
|
246
|
+
name: 'test-integration',
|
|
247
|
+
version: '1.0.0',
|
|
248
|
+
modules: {
|
|
249
|
+
test: {
|
|
250
|
+
definition: {
|
|
251
|
+
moduleName: 'test-module',
|
|
252
|
+
getName: { type: 'function' },
|
|
253
|
+
requiredAuthMethods: {
|
|
254
|
+
getToken: { type: 'function' },
|
|
255
|
+
getCredentialDetails: { type: 'function' },
|
|
256
|
+
getEntityDetails: { type: 'function' },
|
|
257
|
+
apiPropertiesToPersist: {
|
|
258
|
+
credential: [], // empty
|
|
259
|
+
entity: ['external_id']
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const result = validator.validate(definition, 0);
|
|
268
|
+
const warnings = result.getWarnings();
|
|
269
|
+
expect(warnings.some(w => w.code === 'MISSING_CREDENTIAL_PROPS')).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('warns when entity properties are missing', () => {
|
|
273
|
+
const definition = {
|
|
274
|
+
name: 'test-integration',
|
|
275
|
+
version: '1.0.0',
|
|
276
|
+
modules: {
|
|
277
|
+
test: {
|
|
278
|
+
definition: {
|
|
279
|
+
moduleName: 'test-module',
|
|
280
|
+
getName: { type: 'function' },
|
|
281
|
+
requiredAuthMethods: {
|
|
282
|
+
getToken: { type: 'function' },
|
|
283
|
+
getCredentialDetails: { type: 'function' },
|
|
284
|
+
getEntityDetails: { type: 'function' },
|
|
285
|
+
apiPropertiesToPersist: {
|
|
286
|
+
credential: ['access_token'],
|
|
287
|
+
entity: [] // empty
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const result = validator.validate(definition, 0);
|
|
296
|
+
const warnings = result.getWarnings();
|
|
297
|
+
expect(warnings.some(w => w.code === 'MISSING_ENTITY_PROPS')).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('path formatting', () => {
|
|
302
|
+
it('includes correct path prefix for errors', () => {
|
|
303
|
+
const definition = {
|
|
304
|
+
name: 'test-integration',
|
|
305
|
+
version: '1.0.0',
|
|
306
|
+
modules: {
|
|
307
|
+
myModule: {
|
|
308
|
+
definition: {
|
|
309
|
+
// missing required fields
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const result = validator.validate(definition, 2);
|
|
316
|
+
const errors = result.getErrors();
|
|
317
|
+
expect(errors[0].path).toContain('integrations[2].Definition.modules.myModule');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('result type', () => {
|
|
322
|
+
it('returns ValidationResult instance', () => {
|
|
323
|
+
const definition = {
|
|
324
|
+
name: 'test-integration',
|
|
325
|
+
version: '1.0.0'
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = validator.validate(definition, 0);
|
|
329
|
+
expect(result.constructor.name).toBe('ValidationResult');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const { AppDefinitionValidator } = require('../../infrastructure/validators/app-definition-validator');
|
|
2
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
3
|
+
|
|
4
|
+
describe('AppDefinitionValidator', () => {
|
|
5
|
+
let validator;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
validator = new AppDefinitionValidator();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('valid definitions', () => {
|
|
12
|
+
it('validates minimal valid definition', () => {
|
|
13
|
+
const definition = {
|
|
14
|
+
integrations: []
|
|
15
|
+
};
|
|
16
|
+
const result = validator.validate(definition);
|
|
17
|
+
expect(result.isValid()).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('validates definition with database config', () => {
|
|
21
|
+
const definition = {
|
|
22
|
+
integrations: [],
|
|
23
|
+
database: { mongoDB: { enable: true } }
|
|
24
|
+
};
|
|
25
|
+
const result = validator.validate(definition);
|
|
26
|
+
expect(result.isValid()).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('validates definition with integration objects', () => {
|
|
30
|
+
const definition = {
|
|
31
|
+
integrations: [{ Definition: { name: 'test-integration' } }]
|
|
32
|
+
};
|
|
33
|
+
const result = validator.validate(definition);
|
|
34
|
+
expect(result.isValid()).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('validates definition with integration classes (functions)', () => {
|
|
38
|
+
// This tests that integration classes are properly sanitized
|
|
39
|
+
// before JSON Schema validation (classes become stub objects)
|
|
40
|
+
class MyIntegration {
|
|
41
|
+
static Definition = {
|
|
42
|
+
name: 'my-integration',
|
|
43
|
+
version: '1.0.0'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const definition = {
|
|
47
|
+
integrations: [MyIntegration]
|
|
48
|
+
};
|
|
49
|
+
const result = validator.validate(definition);
|
|
50
|
+
// Should not have schema errors about "must be object"
|
|
51
|
+
const schemaErrors = result.getErrors().filter(e =>
|
|
52
|
+
e.message.includes('must be object') ||
|
|
53
|
+
e.message.includes('must be Object')
|
|
54
|
+
);
|
|
55
|
+
expect(schemaErrors).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('validates definition with multiple integration classes', () => {
|
|
59
|
+
class IntegrationA {
|
|
60
|
+
static Definition = { name: 'integration-a', version: '1.0.0' };
|
|
61
|
+
}
|
|
62
|
+
class IntegrationB {
|
|
63
|
+
static Definition = { name: 'integration-b', version: '1.0.0' };
|
|
64
|
+
}
|
|
65
|
+
const definition = {
|
|
66
|
+
integrations: [IntegrationA, IntegrationB]
|
|
67
|
+
};
|
|
68
|
+
const result = validator.validate(definition);
|
|
69
|
+
expect(result.isValid()).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('integrations validation', () => {
|
|
74
|
+
it('errors when integrations is not an array', () => {
|
|
75
|
+
const definition = { integrations: 'not-an-array' };
|
|
76
|
+
const result = validator.validate(definition);
|
|
77
|
+
expect(result.isValid()).toBe(false);
|
|
78
|
+
expect(result.getErrors()[0].path).toBe('integrations');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('errors when integrations is missing', () => {
|
|
82
|
+
const definition = {};
|
|
83
|
+
const result = validator.validate(definition);
|
|
84
|
+
expect(result.isValid()).toBe(false);
|
|
85
|
+
expect(result.getErrors().some(e => e.message.includes('integrations'))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('errors when integration lacks Definition property', () => {
|
|
89
|
+
const BadIntegration = class {};
|
|
90
|
+
const definition = { integrations: [BadIntegration] };
|
|
91
|
+
const result = validator.validate(definition);
|
|
92
|
+
expect(result.isValid()).toBe(false);
|
|
93
|
+
expect(result.getErrors().some(e => e.path === 'integrations[0]' && e.message.includes('Definition'))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('errors when integration Definition lacks name', () => {
|
|
97
|
+
const BadIntegration = class {
|
|
98
|
+
static Definition = {};
|
|
99
|
+
};
|
|
100
|
+
const definition = { integrations: [BadIntegration] };
|
|
101
|
+
const result = validator.validate(definition);
|
|
102
|
+
expect(result.isValid()).toBe(false);
|
|
103
|
+
expect(result.getErrors().some(e => e.path.includes('integrations[0]') && e.message.includes('name'))).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('warns on duplicate integration names', () => {
|
|
107
|
+
const Int1 = class { static Definition = { name: 'same-name', version: '1.0.0' }; };
|
|
108
|
+
const Int2 = class { static Definition = { name: 'same-name', version: '1.0.0' }; };
|
|
109
|
+
const definition = { integrations: [Int1, Int2] };
|
|
110
|
+
const result = validator.validate(definition);
|
|
111
|
+
expect(result.getWarnings().some(w => w.message.includes('duplicate'))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('database validation', () => {
|
|
116
|
+
it('validates mongoDB configuration', () => {
|
|
117
|
+
const definition = {
|
|
118
|
+
integrations: [],
|
|
119
|
+
database: { mongoDB: { enable: true } }
|
|
120
|
+
};
|
|
121
|
+
const result = validator.validate(definition);
|
|
122
|
+
const dbErrors = result.getErrors().filter(e => e.path.startsWith('database'));
|
|
123
|
+
expect(dbErrors).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('validates postgres configuration', () => {
|
|
127
|
+
const definition = {
|
|
128
|
+
integrations: [],
|
|
129
|
+
database: { postgres: { enable: true } }
|
|
130
|
+
};
|
|
131
|
+
const result = validator.validate(definition);
|
|
132
|
+
const dbErrors = result.getErrors().filter(e => e.path.startsWith('database'));
|
|
133
|
+
expect(dbErrors).toHaveLength(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('validates documentDB configuration', () => {
|
|
137
|
+
const definition = {
|
|
138
|
+
integrations: [],
|
|
139
|
+
database: { documentDB: { enable: true } }
|
|
140
|
+
};
|
|
141
|
+
const result = validator.validate(definition);
|
|
142
|
+
const dbErrors = result.getErrors().filter(e => e.path.startsWith('database'));
|
|
143
|
+
expect(dbErrors).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('user configuration', () => {
|
|
148
|
+
it('validates user with password enabled', () => {
|
|
149
|
+
const definition = {
|
|
150
|
+
integrations: [],
|
|
151
|
+
user: { usePassword: true }
|
|
152
|
+
};
|
|
153
|
+
const result = validator.validate(definition);
|
|
154
|
+
const userErrors = result.getErrors().filter(e => e.path.startsWith('user'));
|
|
155
|
+
expect(userErrors).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('validates user with custom model object', () => {
|
|
159
|
+
const definition = {
|
|
160
|
+
integrations: [],
|
|
161
|
+
user: { model: { name: 'CustomUserModel' } }
|
|
162
|
+
};
|
|
163
|
+
const result = validator.validate(definition);
|
|
164
|
+
const userErrors = result.getErrors().filter(e => e.path.startsWith('user'));
|
|
165
|
+
expect(userErrors).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('validates user authModes configuration', () => {
|
|
169
|
+
const definition = {
|
|
170
|
+
integrations: [],
|
|
171
|
+
user: {
|
|
172
|
+
authModes: {
|
|
173
|
+
friggToken: true,
|
|
174
|
+
sharedSecret: true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const result = validator.validate(definition);
|
|
179
|
+
const userErrors = result.getErrors().filter(e => e.path.startsWith('user'));
|
|
180
|
+
expect(userErrors).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('result type', () => {
|
|
185
|
+
it('returns ValidationResult instance', () => {
|
|
186
|
+
const definition = { integrations: [] };
|
|
187
|
+
const result = validator.validate(definition);
|
|
188
|
+
expect(result).toBeInstanceOf(ValidationResult);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|