@friggframework/devtools 2.0.0--canary.666.81647c4.0 → 2.0.0--canary.667.6dc4b39.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/__tests__/management-api-gateway-routes.test.js +281 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -4
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +98 -60
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +19 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +13 -1
- 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,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
|
-
});
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
const { validateCommand, runValidation, preflightValidation } = require('..');
|
|
6
|
-
|
|
7
|
-
const GOOD_INDEX = `
|
|
8
|
-
class Api { static requesterType = 'apiKey'; }
|
|
9
|
-
const definition = {
|
|
10
|
-
moduleName: 'acme',
|
|
11
|
-
API: Api,
|
|
12
|
-
requiredAuthMethods: {
|
|
13
|
-
getEntityDetails: async () => {},
|
|
14
|
-
getCredentialDetails: async () => {},
|
|
15
|
-
testAuthRequest: async () => {},
|
|
16
|
-
apiPropertiesToPersist: { credential: ['api_key'], entity: [] },
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
class AcmeIntegration {
|
|
20
|
-
static Definition = { name: 'acme', version: '1.0.0', modules: { acme: { definition } } };
|
|
21
|
-
}
|
|
22
|
-
module.exports = {
|
|
23
|
-
Definition: {
|
|
24
|
-
name: 'frigg-app',
|
|
25
|
-
integrations: [AcmeIntegration],
|
|
26
|
-
user: { usePassword: true },
|
|
27
|
-
encryption: { fieldLevelEncryptionMethod: 'kms', createResourceIfNoneFound: true },
|
|
28
|
-
database: { postgres: { enable: true, management: 'use-existing', endpoint: 'db.example.com' } },
|
|
29
|
-
environment: { DATABASE_URL: true },
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
`;
|
|
33
|
-
|
|
34
|
-
const BAD_INDEX = `
|
|
35
|
-
module.exports = {
|
|
36
|
-
Definition: {
|
|
37
|
-
integrations: [{ name: 'not-a-class' }],
|
|
38
|
-
database: { postgres: { enable: true, management: 'create-new' } },
|
|
39
|
-
encrpytion: { fieldLevelEncryptionMethod: 'kms' },
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
`;
|
|
43
|
-
|
|
44
|
-
function backend(indexSource) {
|
|
45
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-validate-'));
|
|
46
|
-
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"backend"}');
|
|
47
|
-
fs.writeFileSync(path.join(dir, 'index.js'), indexSource);
|
|
48
|
-
return dir;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('frigg validate', () => {
|
|
52
|
-
const dirs = [];
|
|
53
|
-
let log;
|
|
54
|
-
let warn;
|
|
55
|
-
let error;
|
|
56
|
-
let exit;
|
|
57
|
-
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
log = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
60
|
-
warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
61
|
-
error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
62
|
-
exit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
63
|
-
process.exitCode = undefined;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
afterEach(() => {
|
|
67
|
-
jest.restoreAllMocks();
|
|
68
|
-
process.exitCode = undefined;
|
|
69
|
-
for (const dir of dirs.splice(0))
|
|
70
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const inBackend = (source) => {
|
|
74
|
-
const dir = backend(source);
|
|
75
|
-
dirs.push(dir);
|
|
76
|
-
jest.spyOn(process, 'cwd').mockReturnValue(dir);
|
|
77
|
-
return dir;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
it('passes an app with integration classes and exits 0', async () => {
|
|
81
|
-
inBackend(GOOD_INDEX);
|
|
82
|
-
|
|
83
|
-
const report = await validateCommand({ stage: 'prod' });
|
|
84
|
-
|
|
85
|
-
expect(report.errors).toEqual([]);
|
|
86
|
-
expect(process.exitCode).toBeUndefined();
|
|
87
|
-
expect(log.mock.calls.flat().join('\n')).toContain(
|
|
88
|
-
'The app definition is valid'
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('prints errors with pointers and hints and exits 1', async () => {
|
|
93
|
-
inBackend(BAD_INDEX);
|
|
94
|
-
|
|
95
|
-
const report = await validateCommand({});
|
|
96
|
-
const output = log.mock.calls.flat().join('\n');
|
|
97
|
-
|
|
98
|
-
expect(process.exitCode).toBe(1);
|
|
99
|
-
expect(report.errors.map((e) => e.pointer)).toEqual(
|
|
100
|
-
expect.arrayContaining([
|
|
101
|
-
'/encrpytion',
|
|
102
|
-
'/database/postgres/management',
|
|
103
|
-
'/integrations/0',
|
|
104
|
-
])
|
|
105
|
-
);
|
|
106
|
-
expect(output).toContain('/encrpytion');
|
|
107
|
-
expect(output).toContain('Did you mean "encryption"?');
|
|
108
|
-
expect(output).toContain(
|
|
109
|
-
'Use one of: "discover", "managed", "use-existing"'
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('--json prints a machine-readable report', async () => {
|
|
114
|
-
inBackend(BAD_INDEX);
|
|
115
|
-
|
|
116
|
-
await validateCommand({ json: true, stage: 'prod' });
|
|
117
|
-
const json = JSON.parse(log.mock.calls[0][0]);
|
|
118
|
-
|
|
119
|
-
expect(json).toEqual(
|
|
120
|
-
expect.objectContaining({
|
|
121
|
-
valid: false,
|
|
122
|
-
stage: 'prod',
|
|
123
|
-
errors: expect.arrayContaining([
|
|
124
|
-
expect.objectContaining({
|
|
125
|
-
pointer: '/encrpytion',
|
|
126
|
-
code: 'unknown-key',
|
|
127
|
-
severity: 'error',
|
|
128
|
-
}),
|
|
129
|
-
]),
|
|
130
|
-
warnings: expect.any(Array),
|
|
131
|
-
})
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('reports a backend that fails to load', () => {
|
|
136
|
-
const dir = backend("require('./missing-file');");
|
|
137
|
-
dirs.push(dir);
|
|
138
|
-
|
|
139
|
-
const report = runValidation({ cwd: dir });
|
|
140
|
-
|
|
141
|
-
expect(report.errors).toEqual([
|
|
142
|
-
expect.objectContaining({
|
|
143
|
-
code: 'load-failed',
|
|
144
|
-
message: expect.stringContaining('missing-file'),
|
|
145
|
-
}),
|
|
146
|
-
]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('reports a directory without a backend', () => {
|
|
150
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-empty-'));
|
|
151
|
-
dirs.push(dir);
|
|
152
|
-
|
|
153
|
-
expect(runValidation({ cwd: dir }).errors).toEqual([
|
|
154
|
-
expect.objectContaining({ code: 'backend-not-found' }),
|
|
155
|
-
]);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('finds the backend/ directory from the project root', () => {
|
|
159
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-root-'));
|
|
160
|
-
dirs.push(root);
|
|
161
|
-
fs.mkdirSync(path.join(root, 'backend'));
|
|
162
|
-
fs.writeFileSync(path.join(root, 'backend', 'package.json'), '{}');
|
|
163
|
-
fs.writeFileSync(path.join(root, 'backend', 'index.js'), GOOD_INDEX);
|
|
164
|
-
|
|
165
|
-
expect(runValidation({ cwd: root }).source).toBe(
|
|
166
|
-
path.join(root, 'backend', 'index.js')
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('reports an index.js without a Definition', () => {
|
|
171
|
-
const dir = backend('module.exports = {};');
|
|
172
|
-
dirs.push(dir);
|
|
173
|
-
|
|
174
|
-
expect(runValidation({ cwd: dir }).errors).toEqual([
|
|
175
|
-
expect.objectContaining({ code: 'definition-not-exported' }),
|
|
176
|
-
]);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe('preflightValidation', () => {
|
|
180
|
-
it('stops build/deploy on errors', () => {
|
|
181
|
-
inBackend(BAD_INDEX);
|
|
182
|
-
|
|
183
|
-
const proceed = preflightValidation({
|
|
184
|
-
command: 'deploy',
|
|
185
|
-
options: { stage: 'prod' },
|
|
186
|
-
failOnErrors: true,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
expect(proceed).toBe(false);
|
|
190
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
191
|
-
expect(error.mock.calls.flat().join('\n')).toContain(
|
|
192
|
-
'--skip-validate'
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('lets build/deploy continue on a valid definition', () => {
|
|
197
|
-
inBackend(GOOD_INDEX);
|
|
198
|
-
|
|
199
|
-
expect(
|
|
200
|
-
preflightValidation({
|
|
201
|
-
command: 'build',
|
|
202
|
-
options: { stage: 'prod' },
|
|
203
|
-
failOnErrors: true,
|
|
204
|
-
})
|
|
205
|
-
).toBe(true);
|
|
206
|
-
expect(exit).not.toHaveBeenCalled();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('only warns for start', () => {
|
|
210
|
-
inBackend(BAD_INDEX);
|
|
211
|
-
|
|
212
|
-
expect(
|
|
213
|
-
preflightValidation({
|
|
214
|
-
command: 'start',
|
|
215
|
-
options: {},
|
|
216
|
-
failOnErrors: false,
|
|
217
|
-
})
|
|
218
|
-
).toBe(true);
|
|
219
|
-
expect(exit).not.toHaveBeenCalled();
|
|
220
|
-
const output = log.mock.calls.flat().join('\n');
|
|
221
|
-
expect(output).toContain('warning');
|
|
222
|
-
expect(output).not.toMatch(/^\s+error /m);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('--skip-validate skips validation, loudly', () => {
|
|
226
|
-
inBackend(BAD_INDEX);
|
|
227
|
-
|
|
228
|
-
expect(
|
|
229
|
-
preflightValidation({
|
|
230
|
-
command: 'deploy',
|
|
231
|
-
options: { skipValidate: true },
|
|
232
|
-
failOnErrors: true,
|
|
233
|
-
})
|
|
234
|
-
).toBe(true);
|
|
235
|
-
expect(exit).not.toHaveBeenCalled();
|
|
236
|
-
expect(warn.mock.calls.flat().join('\n')).toContain(
|
|
237
|
-
'VALIDATION SKIPPED'
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|