@friggframework/devtools 2.0.0--canary.666.81647c4.0 → 2.0.0--canary.668.8e53aa9.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 -38
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -3
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +0 -3
- package/frigg-cli/build-command/index.js +0 -6
- package/frigg-cli/deploy-command/index.js +0 -6
- package/frigg-cli/index.js +1 -13
- package/frigg-cli/init-command/backend-first-handler.js +46 -25
- package/frigg-cli/start-command/index.js +0 -4
- package/frigg-cli/start-command/start-command.test.js +0 -3
- package/infrastructure/create-frigg-infrastructure.js +1 -4
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +98 -60
- package/package.json +7 -7
- package/frigg-cli/__tests__/unit/commands/validate-wiring.test.js +0 -92
- package/frigg-cli/validate-command/__tests__/init-validation.test.js +0 -63
- package/frigg-cli/validate-command/__tests__/integrations-checks.test.js +0 -133
- package/frigg-cli/validate-command/__tests__/validate-app-definition.test.js +0 -326
- package/frigg-cli/validate-command/__tests__/validate-command.test.js +0 -241
- package/frigg-cli/validate-command/checks/config.js +0 -468
- package/frigg-cli/validate-command/checks/integrations.js +0 -274
- package/frigg-cli/validate-command/format-report.js +0 -58
- package/frigg-cli/validate-command/index.js +0 -107
- package/frigg-cli/validate-command/load-app-definition.js +0 -93
- package/frigg-cli/validate-command/schema-issues.js +0 -154
- package/frigg-cli/validate-command/validate-app-definition.js +0 -60
- package/infrastructure/app-definition-schema-contract.test.js +0 -607
- package/infrastructure/create-frigg-infrastructure.test.js +0 -51
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* build and deploy validate the app definition first and stop on errors;
|
|
3
|
-
* start validates and only warns (ADR-051).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// child_process and node:child_process are one module to jest: one mock.
|
|
7
|
-
const mockChildProcess = {
|
|
8
|
-
spawnSync: jest.fn(() => ({ status: 0 })),
|
|
9
|
-
spawn: jest.fn(),
|
|
10
|
-
};
|
|
11
|
-
jest.mock('child_process', () => mockChildProcess);
|
|
12
|
-
jest.mock('node:child_process', () => mockChildProcess);
|
|
13
|
-
jest.mock('../../../validate-command', () => ({
|
|
14
|
-
preflightValidation: jest.fn(),
|
|
15
|
-
}));
|
|
16
|
-
jest.mock('../../../doctor-command', () => ({ doctorCommand: jest.fn() }));
|
|
17
|
-
jest.mock('../../../utils/database-validator', () => ({
|
|
18
|
-
validateDatabaseUrl: jest.fn(() => ({ valid: true })),
|
|
19
|
-
getDatabaseType: jest.fn(() => ({ dbType: 'postgresql' })),
|
|
20
|
-
checkPrismaClientGenerated: jest.fn(() => ({ generated: true })),
|
|
21
|
-
}));
|
|
22
|
-
jest.mock('dotenv', () => ({ config: jest.fn() }));
|
|
23
|
-
|
|
24
|
-
const childProcess = mockChildProcess;
|
|
25
|
-
const nodeChildProcess = mockChildProcess;
|
|
26
|
-
const { preflightValidation } = require('../../../validate-command');
|
|
27
|
-
const { buildCommand } = require('../../../build-command');
|
|
28
|
-
const { deployCommand } = require('../../../deploy-command');
|
|
29
|
-
const { startCommand } = require('../../../start-command');
|
|
30
|
-
|
|
31
|
-
describe('validation before build, deploy and start', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
jest.clearAllMocks();
|
|
34
|
-
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
35
|
-
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
36
|
-
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
-
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => jest.restoreAllMocks());
|
|
41
|
-
|
|
42
|
-
it('build validates first and fails on errors', async () => {
|
|
43
|
-
preflightValidation.mockReturnValue(false);
|
|
44
|
-
|
|
45
|
-
await buildCommand({ stage: 'prod' });
|
|
46
|
-
|
|
47
|
-
expect(preflightValidation).toHaveBeenCalledWith({
|
|
48
|
-
command: 'build',
|
|
49
|
-
options: { stage: 'prod' },
|
|
50
|
-
failOnErrors: true,
|
|
51
|
-
});
|
|
52
|
-
expect(childProcess.spawnSync).not.toHaveBeenCalled();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('build packages after a passing validation', async () => {
|
|
56
|
-
preflightValidation.mockReturnValue(true);
|
|
57
|
-
|
|
58
|
-
await buildCommand({ stage: 'prod' });
|
|
59
|
-
|
|
60
|
-
expect(childProcess.spawnSync).toHaveBeenCalled();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('deploy validates first and fails on errors', async () => {
|
|
64
|
-
preflightValidation.mockReturnValue(false);
|
|
65
|
-
|
|
66
|
-
await deployCommand({ stage: 'prod', skipValidate: false });
|
|
67
|
-
|
|
68
|
-
expect(preflightValidation).toHaveBeenCalledWith({
|
|
69
|
-
command: 'deploy',
|
|
70
|
-
options: { stage: 'prod', skipValidate: false },
|
|
71
|
-
failOnErrors: true,
|
|
72
|
-
});
|
|
73
|
-
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('start validates and only warns', async () => {
|
|
77
|
-
preflightValidation.mockReturnValue(true);
|
|
78
|
-
nodeChildProcess.spawn.mockReturnValue({ on: jest.fn() });
|
|
79
|
-
const previous = process.env.DATABASE_URL;
|
|
80
|
-
process.env.DATABASE_URL = 'postgresql://localhost/frigg';
|
|
81
|
-
|
|
82
|
-
await startCommand({ stage: 'dev' });
|
|
83
|
-
|
|
84
|
-
process.env.DATABASE_URL = previous;
|
|
85
|
-
expect(preflightValidation).toHaveBeenCalledWith({
|
|
86
|
-
command: 'start',
|
|
87
|
-
options: { stage: 'dev' },
|
|
88
|
-
failOnErrors: false,
|
|
89
|
-
});
|
|
90
|
-
expect(nodeChildProcess.spawn).toHaveBeenCalled();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `frigg init` validates the generated app with the same code path as
|
|
3
|
-
* `frigg validate`, and never fails because of it (ADR-051).
|
|
4
|
-
*/
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
jest.mock('@inquirer/prompts', () => ({}));
|
|
10
|
-
|
|
11
|
-
const BackendFirstHandler = require('../../init-command/backend-first-handler');
|
|
12
|
-
|
|
13
|
-
function writeApp(source) {
|
|
14
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-init-'));
|
|
15
|
-
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"backend"}');
|
|
16
|
-
fs.writeFileSync(path.join(dir, 'index.js'), source);
|
|
17
|
-
return dir;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('frigg init validation', () => {
|
|
21
|
-
const dirs = [];
|
|
22
|
-
let log;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
log = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
jest.restoreAllMocks();
|
|
30
|
-
for (const dir of dirs.splice(0))
|
|
31
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('validates the real generated definition, not a stand-in', async () => {
|
|
35
|
-
const dir = writeApp(
|
|
36
|
-
"module.exports = { Definition: { integrations: [], database: { postgres: { enable: true, management: 'create-new' } } } };"
|
|
37
|
-
);
|
|
38
|
-
dirs.push(dir);
|
|
39
|
-
const handler = new BackendFirstHandler(dir, {});
|
|
40
|
-
|
|
41
|
-
const ok = await handler.validateGeneratedAppDefinition(
|
|
42
|
-
path.join(dir, 'index.js')
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
expect(ok).toBe(false);
|
|
46
|
-
expect(log.mock.calls.flat().join('\n')).toContain(
|
|
47
|
-
'/database/postgres/management'
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('stays quiet before npm install, when index.js cannot load its dependencies', async () => {
|
|
52
|
-
const dir = writeApp(
|
|
53
|
-
"require('@friggframework/api-module-does-not-exist');"
|
|
54
|
-
);
|
|
55
|
-
dirs.push(dir);
|
|
56
|
-
const handler = new BackendFirstHandler(dir, {});
|
|
57
|
-
|
|
58
|
-
await expect(
|
|
59
|
-
handler.validateGeneratedAppDefinition(path.join(dir, 'index.js'))
|
|
60
|
-
).resolves.toBe(true);
|
|
61
|
-
expect(log).not.toHaveBeenCalled();
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
const { checkIntegrations } = require('../checks/integrations');
|
|
2
|
-
|
|
3
|
-
class FakeApi {
|
|
4
|
-
static requesterType = 'oauth2';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const moduleDefinition = () => ({
|
|
8
|
-
moduleName: 'acme',
|
|
9
|
-
API: FakeApi,
|
|
10
|
-
requiredAuthMethods: {
|
|
11
|
-
getToken: async () => {},
|
|
12
|
-
getEntityDetails: async () => {},
|
|
13
|
-
getCredentialDetails: async () => {},
|
|
14
|
-
testAuthRequest: async () => {},
|
|
15
|
-
apiPropertiesToPersist: { credential: [], entity: [] },
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function integrationClass(definition) {
|
|
20
|
-
return class Integration {
|
|
21
|
-
static Definition = definition;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const goodIntegration = () =>
|
|
26
|
-
integrationClass({
|
|
27
|
-
name: 'acme',
|
|
28
|
-
version: '1.0.0',
|
|
29
|
-
modules: { acme: { definition: moduleDefinition() } },
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const codes = (issues) =>
|
|
33
|
-
issues.map((i) => `${i.severity}:${i.code}:${i.pointer}`);
|
|
34
|
-
|
|
35
|
-
describe('checkIntegrations', () => {
|
|
36
|
-
it('accepts a well-formed integration class', () => {
|
|
37
|
-
expect(
|
|
38
|
-
checkIntegrations({ integrations: [goodIntegration()] })
|
|
39
|
-
).toEqual([]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('warns when the app lists no integrations', () => {
|
|
43
|
-
expect(codes(checkIntegrations({ integrations: [] }))).toEqual([
|
|
44
|
-
'warning:no-integrations:/integrations',
|
|
45
|
-
]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('rejects an entry that is not a class', () => {
|
|
49
|
-
expect(
|
|
50
|
-
codes(
|
|
51
|
-
checkIntegrations({
|
|
52
|
-
integrations: [{ Definition: { name: 'x' } }],
|
|
53
|
-
})
|
|
54
|
-
)
|
|
55
|
-
).toEqual(['error:integration-not-a-class:/integrations/0']);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('rejects a class without a static Definition', () => {
|
|
59
|
-
class Bare {}
|
|
60
|
-
expect(codes(checkIntegrations({ integrations: [Bare] }))).toEqual([
|
|
61
|
-
'error:integration-definition-missing:/integrations/0/Definition',
|
|
62
|
-
]);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('requires a valid name and modules, and warns on a missing version', () => {
|
|
66
|
-
const Cls = integrationClass({ name: '1 bad name' });
|
|
67
|
-
expect(codes(checkIntegrations({ integrations: [Cls] }))).toEqual([
|
|
68
|
-
'error:integration-name-invalid:/integrations/0/Definition/name',
|
|
69
|
-
'warning:integration-version-missing:/integrations/0/Definition/version',
|
|
70
|
-
'error:integration-modules-missing:/integrations/0/Definition/modules',
|
|
71
|
-
]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('reports duplicate integration names', () => {
|
|
75
|
-
expect(
|
|
76
|
-
codes(
|
|
77
|
-
checkIntegrations({
|
|
78
|
-
integrations: [goodIntegration(), goodIntegration()],
|
|
79
|
-
})
|
|
80
|
-
)
|
|
81
|
-
).toEqual([
|
|
82
|
-
'error:integration-name-duplicate:/integrations/1/Definition/name',
|
|
83
|
-
]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('checks module definitions the way Module.validateDefinition does', () => {
|
|
87
|
-
const definition = moduleDefinition();
|
|
88
|
-
delete definition.requiredAuthMethods.getToken;
|
|
89
|
-
delete definition.moduleName;
|
|
90
|
-
const Cls = integrationClass({
|
|
91
|
-
name: 'acme',
|
|
92
|
-
version: '1',
|
|
93
|
-
modules: { acme: { definition }, other: {} },
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(codes(checkIntegrations({ integrations: [Cls] }))).toEqual([
|
|
97
|
-
'error:module-name-missing:/integrations/0/Definition/modules/acme/definition/moduleName',
|
|
98
|
-
'error:module-auth-method-missing:/integrations/0/Definition/modules/acme/definition/requiredAuthMethods/getToken',
|
|
99
|
-
'error:module-definition-missing:/integrations/0/Definition/modules/other/definition',
|
|
100
|
-
]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('does not require getToken for non-OAuth2 modules', () => {
|
|
104
|
-
const definition = moduleDefinition();
|
|
105
|
-
definition.API = class {
|
|
106
|
-
static requesterType = 'apiKey';
|
|
107
|
-
};
|
|
108
|
-
delete definition.requiredAuthMethods.getToken;
|
|
109
|
-
const Cls = integrationClass({
|
|
110
|
-
name: 'acme',
|
|
111
|
-
version: '1',
|
|
112
|
-
modules: { acme: { definition } },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(checkIntegrations({ integrations: [Cls] })).toEqual([]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('warns that a module rateLimit is not applied', () => {
|
|
119
|
-
const definition = {
|
|
120
|
-
...moduleDefinition(),
|
|
121
|
-
rateLimit: { requests: 10, period: 'second' },
|
|
122
|
-
};
|
|
123
|
-
const Cls = integrationClass({
|
|
124
|
-
name: 'acme',
|
|
125
|
-
version: '1',
|
|
126
|
-
modules: { acme: { definition } },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(codes(checkIntegrations({ integrations: [Cls] }))).toEqual([
|
|
130
|
-
'warning:module-rate-limit-ignored:/integrations/0/Definition/modules/acme/definition/rateLimit',
|
|
131
|
-
]);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
const { validateDefinition } = require('../validate-app-definition');
|
|
2
|
-
|
|
3
|
-
class Api {
|
|
4
|
-
static requesterType = 'oauth2';
|
|
5
|
-
}
|
|
6
|
-
const auth = {
|
|
7
|
-
getToken: async () => {},
|
|
8
|
-
getEntityDetails: async () => {},
|
|
9
|
-
getCredentialDetails: async () => {},
|
|
10
|
-
testAuthRequest: async () => {},
|
|
11
|
-
apiPropertiesToPersist: { credential: [], entity: [] },
|
|
12
|
-
};
|
|
13
|
-
class AcmeIntegration {
|
|
14
|
-
static Definition = {
|
|
15
|
-
name: 'acme',
|
|
16
|
-
version: '1.0.0',
|
|
17
|
-
modules: {
|
|
18
|
-
acme: {
|
|
19
|
-
definition: {
|
|
20
|
-
moduleName: 'acme',
|
|
21
|
-
API: Api,
|
|
22
|
-
requiredAuthMethods: auth,
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The app `frigg init` scaffolds, with one integration installed. */
|
|
30
|
-
const scaffold = (overrides = {}) => ({
|
|
31
|
-
name: 'frigg-app',
|
|
32
|
-
integrations: [AcmeIntegration],
|
|
33
|
-
user: {
|
|
34
|
-
usePassword: true,
|
|
35
|
-
primary: 'individual',
|
|
36
|
-
individualUserRequired: true,
|
|
37
|
-
organizationUserRequired: false,
|
|
38
|
-
},
|
|
39
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
40
|
-
managementMode: 'managed',
|
|
41
|
-
vpcIsolation: 'isolated',
|
|
42
|
-
vpc: { enable: false },
|
|
43
|
-
database: {
|
|
44
|
-
postgres: {
|
|
45
|
-
enable: true,
|
|
46
|
-
management: 'use-existing',
|
|
47
|
-
endpoint: 'db.example.com',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
environment: { DATABASE_URL: true },
|
|
51
|
-
logging: { retentionInDays: 14 },
|
|
52
|
-
ssm: { enable: false },
|
|
53
|
-
...overrides,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const summary = ({ errors, warnings }) => ({
|
|
57
|
-
errors: errors.map((i) => `${i.code} ${i.pointer}`),
|
|
58
|
-
warnings: warnings.map((i) => `${i.code} ${i.pointer}`),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('validateDefinition', () => {
|
|
62
|
-
it('passes a scaffolded app with an integration class', () => {
|
|
63
|
-
expect(
|
|
64
|
-
summary(
|
|
65
|
-
validateDefinition({
|
|
66
|
-
definition: scaffold(),
|
|
67
|
-
stage: 'prod',
|
|
68
|
-
env: {},
|
|
69
|
-
})
|
|
70
|
-
)
|
|
71
|
-
).toEqual({
|
|
72
|
-
errors: [],
|
|
73
|
-
warnings: ['no-effect /database/postgres/management'],
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('reports schema errors with JSON pointers and fix hints', () => {
|
|
78
|
-
const result = validateDefinition({
|
|
79
|
-
definition: scaffold({
|
|
80
|
-
vpc: { enable: false, mangement: 'discover' },
|
|
81
|
-
logging: { level: 'loud' },
|
|
82
|
-
}),
|
|
83
|
-
env: {},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
expect(result.errors).toEqual([
|
|
87
|
-
expect.objectContaining({
|
|
88
|
-
code: 'unknown-key',
|
|
89
|
-
pointer: '/vpc/mangement',
|
|
90
|
-
hint: 'Did you mean "management"?',
|
|
91
|
-
}),
|
|
92
|
-
expect.objectContaining({
|
|
93
|
-
code: 'invalid-value',
|
|
94
|
-
pointer: '/logging/level',
|
|
95
|
-
hint: expect.stringContaining('"debug"'),
|
|
96
|
-
}),
|
|
97
|
-
]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("rejects database.postgres.management 'existing', which the Aurora builder rejects", () => {
|
|
101
|
-
const definition = scaffold({
|
|
102
|
-
managementMode: undefined,
|
|
103
|
-
database: { postgres: { enable: true, management: 'existing' } },
|
|
104
|
-
vpc: { enable: true },
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expect(
|
|
108
|
-
summary(validateDefinition({ definition, env: {} })).errors
|
|
109
|
-
).toEqual(['invalid-value /database/postgres/management']);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('reports a broken integration class', () => {
|
|
113
|
-
class Broken {
|
|
114
|
-
static Definition = { name: 'broken' };
|
|
115
|
-
}
|
|
116
|
-
const result = validateDefinition({
|
|
117
|
-
definition: scaffold({ integrations: [Broken] }),
|
|
118
|
-
env: {},
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
expect(summary(result).errors).toEqual([
|
|
122
|
-
'integration-modules-missing /integrations/0/Definition/modules',
|
|
123
|
-
]);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('reports an integration that is not a class once', () => {
|
|
127
|
-
const result = validateDefinition({
|
|
128
|
-
definition: scaffold({ integrations: ['acme'] }),
|
|
129
|
-
env: {},
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(summary(result).errors).toEqual([
|
|
133
|
-
'integration-not-a-class /integrations/0',
|
|
134
|
-
]);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('reports a missing database', () => {
|
|
138
|
-
const result = validateDefinition({
|
|
139
|
-
definition: scaffold({ database: undefined }),
|
|
140
|
-
env: {},
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(summary(result).errors).toEqual(['no-database /database']);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('a non-object definition is a schema error, not a crash', () => {
|
|
147
|
-
expect(
|
|
148
|
-
summary(validateDefinition({ definition: 'nope', env: {} })).errors
|
|
149
|
-
).toEqual(['invalid-type ']);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe('semantic checks', () => {
|
|
154
|
-
const run = (overrides, options = {}) =>
|
|
155
|
-
summary(
|
|
156
|
-
validateDefinition({
|
|
157
|
-
definition: scaffold(overrides),
|
|
158
|
-
env: {},
|
|
159
|
-
...options,
|
|
160
|
-
})
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
describe('encryption', () => {
|
|
164
|
-
it("requires AES_KEY_ID and AES_KEY in environment for 'aes'", () => {
|
|
165
|
-
expect(
|
|
166
|
-
run({ encryption: { fieldLevelEncryptionMethod: 'aes' } })
|
|
167
|
-
.errors
|
|
168
|
-
).toEqual([
|
|
169
|
-
'missing-encryption-env /environment/AES_KEY_ID',
|
|
170
|
-
'missing-encryption-env /environment/AES_KEY',
|
|
171
|
-
]);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('warns when the AES values are missing from the deploying shell', () => {
|
|
175
|
-
const result = run(
|
|
176
|
-
{
|
|
177
|
-
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
178
|
-
environment: {
|
|
179
|
-
DATABASE_URL: true,
|
|
180
|
-
AES_KEY_ID: true,
|
|
181
|
-
AES_KEY: true,
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
{ stage: 'prod', env: { AES_KEY_ID: 'k1' } }
|
|
185
|
-
);
|
|
186
|
-
expect(result.errors).toEqual([]);
|
|
187
|
-
expect(result.warnings).toContain(
|
|
188
|
-
'missing-env-value /environment/AES_KEY'
|
|
189
|
-
);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('warns that a deployed stage without a method stores plaintext', () => {
|
|
193
|
-
expect(
|
|
194
|
-
run({ encryption: undefined }, { stage: 'prod' }).warnings
|
|
195
|
-
).toContain(
|
|
196
|
-
'no-field-encryption /encryption/fieldLevelEncryptionMethod'
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('warns that stage dev never encrypts', () => {
|
|
201
|
-
expect(run({}, { stage: 'dev' }).warnings).toContain(
|
|
202
|
-
'encryption-bypassed /encryption/fieldLevelEncryptionMethod'
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('warns about the KMS key fallback outside managed mode', () => {
|
|
207
|
-
expect(
|
|
208
|
-
run({ managementMode: undefined, vpcIsolation: undefined })
|
|
209
|
-
.warnings
|
|
210
|
-
).toContain('kms-key-source /encryption/createResourceIfNoneFound');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe('user', () => {
|
|
215
|
-
it('rejects a user config with which login always fails', () => {
|
|
216
|
-
expect(
|
|
217
|
-
run({ user: { individualUserRequired: false } }).errors
|
|
218
|
-
).toEqual(['no-login /user']);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('warns an organization-only app that individualUserRequired now defaults to true', () => {
|
|
222
|
-
expect(
|
|
223
|
-
run({ user: { organizationUserRequired: true } }).warnings
|
|
224
|
-
).toContain('default-changed /user/individualUserRequired');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('rejects adopterJwt without a secret and an app with every auth mode off', () => {
|
|
228
|
-
expect(
|
|
229
|
-
run({
|
|
230
|
-
user: {
|
|
231
|
-
authModes: {
|
|
232
|
-
friggToken: false,
|
|
233
|
-
sharedSecret: false,
|
|
234
|
-
adopterJwt: false,
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
}).errors
|
|
238
|
-
).toEqual(['no-auth-mode /user/authModes']);
|
|
239
|
-
expect(
|
|
240
|
-
run({ user: { authModes: { adopterJwt: true } } }).errors
|
|
241
|
-
).toEqual(['missing-key /user/jwtConfig/secret']);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('warns that user.password is the old spelling of usePassword', () => {
|
|
245
|
-
expect(run({ user: { password: true } }).warnings).toContain(
|
|
246
|
-
'deprecated-alias /user/password'
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe('management and VPC', () => {
|
|
252
|
-
it("warns that 'managed' does not stop the VPC builder applying vpc.management", () => {
|
|
253
|
-
expect(
|
|
254
|
-
run({ vpc: { enable: true, management: 'discover' } }).warnings
|
|
255
|
-
).toContain('conflicting-settings /vpc/management');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("warns when 'managed' is used without vpcIsolation", () => {
|
|
259
|
-
expect(run({ vpcIsolation: undefined }).warnings).toContain(
|
|
260
|
-
'ambiguous-default /vpcIsolation'
|
|
261
|
-
);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('warns about subnet and NAT values the VPC builder ignores', () => {
|
|
265
|
-
const result = run({
|
|
266
|
-
managementMode: undefined,
|
|
267
|
-
vpc: {
|
|
268
|
-
enable: true,
|
|
269
|
-
subnets: { management: 'create-new' },
|
|
270
|
-
natGateway: { management: 'discover' },
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
expect(result.warnings).toEqual(
|
|
274
|
-
expect.arrayContaining([
|
|
275
|
-
'no-effect /vpc/subnets/management',
|
|
276
|
-
'no-effect /vpc/natGateway/management',
|
|
277
|
-
])
|
|
278
|
-
);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('requires the ids use-existing needs', () => {
|
|
282
|
-
const result = run({
|
|
283
|
-
managementMode: undefined,
|
|
284
|
-
vpc: {
|
|
285
|
-
enable: true,
|
|
286
|
-
management: 'use-existing',
|
|
287
|
-
subnets: { management: 'use-existing' },
|
|
288
|
-
},
|
|
289
|
-
database: {
|
|
290
|
-
postgres: { enable: true, management: 'use-existing' },
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
expect(result.errors).toEqual([
|
|
294
|
-
'missing-key /vpc/vpcId',
|
|
295
|
-
'missing-key /vpc/subnets/ids',
|
|
296
|
-
'missing-key /database/postgres/endpoint',
|
|
297
|
-
]);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('warns that a private Aurora cluster is unreachable from Lambdas outside a VPC', () => {
|
|
301
|
-
expect(
|
|
302
|
-
run({
|
|
303
|
-
managementMode: undefined,
|
|
304
|
-
database: {
|
|
305
|
-
postgres: { enable: true, management: 'managed' },
|
|
306
|
-
},
|
|
307
|
-
}).warnings
|
|
308
|
-
).toContain('unreachable-database /vpc/enable');
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('warns about keys nothing reads', () => {
|
|
313
|
-
expect(
|
|
314
|
-
run({ custom: { appName: 'x' }, security: { cors: {} } }).warnings
|
|
315
|
-
).toEqual(
|
|
316
|
-
expect.arrayContaining(['no-effect /security', 'no-effect /custom'])
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("warns that 'ssm' environment entries need ssm.enable", () => {
|
|
321
|
-
expect(
|
|
322
|
-
run({ environment: { DATABASE_URL: true, API_KEY: 'ssm' } })
|
|
323
|
-
.warnings
|
|
324
|
-
).toContain('ssm-inactive /environment/API_KEY');
|
|
325
|
-
});
|
|
326
|
-
});
|