@friggframework/devtools 1.2.0-canary.293.50b9cd8.0 → 1.2.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/.eslintrc.json +3 -0
- package/CHANGELOG.md +117 -0
- package/README.md +80 -0
- package/frigg-cli/backendJs.js +33 -0
- package/frigg-cli/backendPath.js +26 -0
- package/frigg-cli/commitChanges.js +16 -0
- package/frigg-cli/environmentVariables.js +134 -0
- package/frigg-cli/environmentVariables.test.js +86 -0
- package/frigg-cli/index.js +14 -0
- package/frigg-cli/index.test.js +109 -0
- package/frigg-cli/installCommand.js +57 -0
- package/frigg-cli/installPackage.js +13 -0
- package/frigg-cli/integrationFile.js +30 -0
- package/frigg-cli/logger.js +12 -0
- package/frigg-cli/template.js +90 -0
- package/frigg-cli/validatePackage.js +79 -0
- package/index.js +2 -6
- package/package.json +16 -7
- package/{test-environment → test}/auther-definition-tester.js +9 -5
- package/test/index.js +11 -0
- package/test/mock-api-readme.md +102 -0
- package/test/mock-api.js +284 -0
- package/{test-environment → test}/mock-integration.js +10 -8
- package/migrations/README.md +0 -3
- package/migrations/bump3.txt +0 -0
- package/migrations/jest.config.js +0 -3
- package/test-environment/Authenticator.js +0 -74
- package/test-environment/index.js +0 -25
- /package/{test-environment → test}/auther-definition-method-tester.js +0 -0
- /package/{test-environment → test}/integration-validator.js +0 -0
package/test/mock-api.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const nock = require('nock');
|
|
2
|
+
const { Authenticator } = require('@friggframework/test');
|
|
3
|
+
const { join: joinPath } = require('path');
|
|
4
|
+
const { parse: parseUrl } = require('url');
|
|
5
|
+
const { mkdir, readFile, rename, rm, writeFile } = require('fs/promises');
|
|
6
|
+
|
|
7
|
+
// TODO store in DB?
|
|
8
|
+
const tokenDirectory = joinPath(process.cwd(), 'test', '.token-cache');
|
|
9
|
+
const fixtureDirectory = joinPath(process.cwd(), 'test', 'recorded-requests');
|
|
10
|
+
nock.back.fixtures = fixtureDirectory;
|
|
11
|
+
|
|
12
|
+
// Try to rename but fail silently if the file does not exist.
|
|
13
|
+
const tryRename = async (a, b) => {
|
|
14
|
+
try {
|
|
15
|
+
await rename(a, b);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code === 'ENOENT') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getJestGlobalState = () => {
|
|
24
|
+
const globalSymbols = Object.getOwnPropertySymbols(global);
|
|
25
|
+
let jestState;
|
|
26
|
+
globalSymbols.forEach((sym) => {
|
|
27
|
+
if (sym.toString() === 'Symbol(JEST_STATE_SYMBOL)') {
|
|
28
|
+
jestState = global[sym];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return jestState;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const checkForOnlies = () => {
|
|
36
|
+
let didFindOnly = false;
|
|
37
|
+
const findOnly = (child) => {
|
|
38
|
+
if (child.mode === 'only') {
|
|
39
|
+
didFindOnly = true;
|
|
40
|
+
}
|
|
41
|
+
if (child.children) {
|
|
42
|
+
child.children.forEach((nestedChild) => {
|
|
43
|
+
findOnly(nestedChild);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const jestState = getJestGlobalState();
|
|
48
|
+
const rootDescribe = jestState.rootDescribeBlock;
|
|
49
|
+
|
|
50
|
+
for (const child of rootDescribe.children) {
|
|
51
|
+
findOnly(child);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return didFindOnly;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mockApi = (Api, classOptionByName = {}) => {
|
|
58
|
+
const {
|
|
59
|
+
authenticationMode,
|
|
60
|
+
displayName = Api.name,
|
|
61
|
+
filteringScope,
|
|
62
|
+
} = classOptionByName;
|
|
63
|
+
// The tag is the lower case display name, with any trailing 'Api' in the string removed.
|
|
64
|
+
const tag = displayName.replace(/Api$/i, '').toLowerCase();
|
|
65
|
+
const tokenFile = `${displayName}.json`;
|
|
66
|
+
const tokenFileFullPath = joinPath(tokenDirectory, tokenFile);
|
|
67
|
+
|
|
68
|
+
return class MockedApi extends Api {
|
|
69
|
+
static name = `Mocked${displayName}`;
|
|
70
|
+
static tokenResponse = null;
|
|
71
|
+
static excludedRecordingPaths = [];
|
|
72
|
+
static #context = {};
|
|
73
|
+
|
|
74
|
+
static async initialize() {
|
|
75
|
+
this.#context = {};
|
|
76
|
+
|
|
77
|
+
const didFindOnlies = checkForOnlies();
|
|
78
|
+
|
|
79
|
+
if (didFindOnlies) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'Cancelled recording API mocks because some tests were marked `.only`. Please remove any `.only`s from any `describe` blocks deeper than the root level, and all `it` blocks.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.#context.originalNockMode = nock.back.currentMode;
|
|
86
|
+
|
|
87
|
+
const { npm_config_record_apis: apisToRecordText = '' } =
|
|
88
|
+
process.env;
|
|
89
|
+
const apisToRecord = apisToRecordText
|
|
90
|
+
.split(',')
|
|
91
|
+
.map((name) => name.trim().toLowerCase());
|
|
92
|
+
|
|
93
|
+
if (apisToRecord.includes(tag)) {
|
|
94
|
+
this.#context.nockMode = 'update';
|
|
95
|
+
} else {
|
|
96
|
+
this.#context.nockMode = 'lockdown';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
nock.back.setMode(this.#context.nockMode);
|
|
100
|
+
|
|
101
|
+
const fixtureFile = `${displayName}.json`;
|
|
102
|
+
|
|
103
|
+
if (this.#context.nockMode === 'update') {
|
|
104
|
+
const fixtureFileFullPath = joinPath(
|
|
105
|
+
fixtureDirectory,
|
|
106
|
+
fixtureFile
|
|
107
|
+
);
|
|
108
|
+
const fixtureFileBackupFullPath = joinPath(
|
|
109
|
+
fixtureDirectory,
|
|
110
|
+
`.${displayName}.json.backup`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await tryRename(fixtureFileFullPath, fixtureFileBackupFullPath);
|
|
114
|
+
|
|
115
|
+
this.#context.restoreFixture = async () =>
|
|
116
|
+
await tryRename(
|
|
117
|
+
fixtureFileBackupFullPath,
|
|
118
|
+
fixtureFileFullPath
|
|
119
|
+
);
|
|
120
|
+
this.#context.deleteFixtureBackup = async () =>
|
|
121
|
+
await rm(fixtureFileBackupFullPath, { force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nockBack = await nock.back(fixtureFile, {
|
|
125
|
+
before: (scope) => {
|
|
126
|
+
if (filteringScope) {
|
|
127
|
+
scope.options.filteringScope = filteringScope;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
// Filter out token URLs
|
|
131
|
+
afterRecord: (recordings) =>
|
|
132
|
+
recordings.filter(
|
|
133
|
+
({ path }) =>
|
|
134
|
+
!this.excludedRecordingPaths.includes(path)
|
|
135
|
+
),
|
|
136
|
+
recorder: {
|
|
137
|
+
output_objects: true,
|
|
138
|
+
enable_reqheaders_recording: false,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this.#context.assertAllRequests = () =>
|
|
143
|
+
nockBack.context.assertScopesFinished();
|
|
144
|
+
this.#context.done = () => nockBack.nockDone();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static async clean() {
|
|
148
|
+
const {
|
|
149
|
+
assertAllRequests,
|
|
150
|
+
done,
|
|
151
|
+
nockMode,
|
|
152
|
+
originalNockMode,
|
|
153
|
+
restoreFixture,
|
|
154
|
+
deleteFixtureBackup,
|
|
155
|
+
} = this.#context;
|
|
156
|
+
|
|
157
|
+
const { didAllTestsPass } = global.mockApiResults;
|
|
158
|
+
|
|
159
|
+
if (done) {
|
|
160
|
+
done();
|
|
161
|
+
}
|
|
162
|
+
if (originalNockMode) {
|
|
163
|
+
nock.back.setMode(originalNockMode);
|
|
164
|
+
}
|
|
165
|
+
if (assertAllRequests && nockMode !== 'update') {
|
|
166
|
+
assertAllRequests();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
nock.cleanAll();
|
|
170
|
+
nock.restore();
|
|
171
|
+
|
|
172
|
+
if (nockMode === 'update') {
|
|
173
|
+
if (!didAllTestsPass) {
|
|
174
|
+
try {
|
|
175
|
+
await restoreFixture();
|
|
176
|
+
} finally {
|
|
177
|
+
throw new Error(
|
|
178
|
+
'Cancelled recording API mocks because some tests failed. Please fix the failing tests and try to record again.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
await deleteFixtureBackup();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static async saveCachedTokenResponse() {
|
|
188
|
+
if (!this.tokenResponse) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await mkdir(tokenDirectory, { recursive: true });
|
|
193
|
+
await writeFile(
|
|
194
|
+
tokenFileFullPath,
|
|
195
|
+
JSON.stringify(this.tokenResponse)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static async loadCachedTokenResponse() {
|
|
200
|
+
try {
|
|
201
|
+
const tokenResponseText = await readFile(tokenFileFullPath);
|
|
202
|
+
this.tokenResponse = JSON.parse(tokenResponseText);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error.code === 'ENOENT') {
|
|
205
|
+
this.tokenResponse = null;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static async mock(...constructorParameters) {
|
|
213
|
+
const api = new this(...constructorParameters);
|
|
214
|
+
|
|
215
|
+
if (nock.back.currentMode !== 'lockdown') {
|
|
216
|
+
await this.loadCachedTokenResponse();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// TODO read authentication mode from module package
|
|
220
|
+
if (authenticationMode === 'client_credentials') {
|
|
221
|
+
// TODO make generic (tied to crossbeam api)
|
|
222
|
+
api.grantType = 'client_credentials';
|
|
223
|
+
api.refreshAccessToken = api.getTokenFromClientCredentials;
|
|
224
|
+
|
|
225
|
+
if (process.env.CROSSBEAM_API_BASE_URL)
|
|
226
|
+
api.baseUrl = process.env.CROSSBEAM_API_BASE_URL;
|
|
227
|
+
if (process.env.CROSSBEAM_API_AUTH_URL)
|
|
228
|
+
api.tokenUri = `${process.env.CROSSBEAM_API_AUTH_URL}/oauth/token`;
|
|
229
|
+
if (process.env.CROSSBEAM_API_AUDIENCE)
|
|
230
|
+
api.audience = process.env.CROSSBEAM_API_AUDIENCE;
|
|
231
|
+
|
|
232
|
+
api.client_secret = process.env.CROSSBEAM_TEST_CLIENT_SECRET;
|
|
233
|
+
api.client_id = process.env.CROSSBEAM_TEST_CLIENT_ID;
|
|
234
|
+
api.refreshAccessToken = api.getTokenFromClientCredentials;
|
|
235
|
+
|
|
236
|
+
this.tokenResponse = await api.getTokenFromClientCredentials();
|
|
237
|
+
} else if (authenticationMode === 'puppet') {
|
|
238
|
+
throw new Error('Not yet implemented');
|
|
239
|
+
} else if (authenticationMode === 'browser') {
|
|
240
|
+
if (nock.back.currentMode !== 'lockdown') {
|
|
241
|
+
const { path: tokenPath } = parseUrl(api.tokenUri);
|
|
242
|
+
this.excludedRecordingPaths.push(tokenPath);
|
|
243
|
+
|
|
244
|
+
if (this.tokenResponse) {
|
|
245
|
+
await api.setTokens(this.tokenResponse);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await api.testAuth();
|
|
249
|
+
} catch {
|
|
250
|
+
this.tokenResponse = null;
|
|
251
|
+
nock.cleanAll();
|
|
252
|
+
await rm(tokenFileFullPath, {
|
|
253
|
+
force: true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!this.tokenResponse) {
|
|
259
|
+
const url = api.authorizationUri;
|
|
260
|
+
const { data } = await Authenticator.oauth2(url);
|
|
261
|
+
const { code } = data;
|
|
262
|
+
this.tokenResponse = await api.getTokenFromCode(code);
|
|
263
|
+
await api.setTokens(this.tokenResponse);
|
|
264
|
+
nock.cleanAll();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else if (authenticationMode === 'manual') {
|
|
268
|
+
// NOOP. This space intentionally left blank. No action should be performed in this mode, and the developer writing the test will handle authentication externally to this module.
|
|
269
|
+
} else {
|
|
270
|
+
throw new Error(
|
|
271
|
+
'Unrecognized authentication mode for mocked API.'
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (nock.back.currentMode !== 'lockdown') {
|
|
276
|
+
await this.saveCachedTokenResponse();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return api;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
module.exports = { mockApi };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const { Auther, Credential, Entity,
|
|
1
|
+
const { Auther, Credential, Entity, IntegrationFactory, createObjectId } = require('@friggframework/core');
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
async function createMockIntegration(IntegrationClassDef, userId = null, config = {},) {
|
|
4
|
-
const
|
|
5
|
+
const integrationFactory = new IntegrationFactory([IntegrationClassDef]);
|
|
5
6
|
userId = userId || createObjectId();
|
|
6
|
-
integration.delegateTypes.push(...IntegrationClassDef.Config.events)
|
|
7
7
|
|
|
8
8
|
const insertOptions = {
|
|
9
9
|
new: true,
|
|
@@ -41,11 +41,13 @@ async function createMockIntegration(IntegrationClassDef, userId = null, config
|
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
const entities = [entity1, entity2]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
|
|
45
|
+
const integration =
|
|
46
|
+
await integrationFactory.createIntegration(
|
|
47
|
+
entities,
|
|
48
|
+
userId,
|
|
49
|
+
config,
|
|
50
|
+
);
|
|
49
51
|
|
|
50
52
|
integration.id = integration.record._id
|
|
51
53
|
|
package/migrations/README.md
DELETED
package/migrations/bump3.txt
DELETED
|
File without changes
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// "use strict";
|
|
2
|
-
const http = require('http');
|
|
3
|
-
const url = require('url');
|
|
4
|
-
const open = require('open');
|
|
5
|
-
|
|
6
|
-
class Authenticator {
|
|
7
|
-
static searchParamsToDictionary(params) {
|
|
8
|
-
const entries = params.entries();
|
|
9
|
-
const result = {};
|
|
10
|
-
for (const entry of entries) {
|
|
11
|
-
const [key, value] = entry;
|
|
12
|
-
result[key] = value;
|
|
13
|
-
}
|
|
14
|
-
return result;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
static async oauth2(authorizeUrl, port = 3000, browserName = undefined) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const server = http
|
|
20
|
-
.createServer(async (req, res) => {
|
|
21
|
-
try {
|
|
22
|
-
const qs = new url.URL(
|
|
23
|
-
req.url,
|
|
24
|
-
`http://localhost:${port}`
|
|
25
|
-
).searchParams;
|
|
26
|
-
|
|
27
|
-
// gets the last parameter in the slash
|
|
28
|
-
const urlPostfix = req.url.split('?')[0];
|
|
29
|
-
|
|
30
|
-
const params =
|
|
31
|
-
Authenticator.searchParamsToDictionary(qs);
|
|
32
|
-
|
|
33
|
-
res.end(
|
|
34
|
-
`<h1 style="position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);">Feel free to close the Brow Brow now</h1>
|
|
35
|
-
<style>@media (prefers-color-scheme: dark) {
|
|
36
|
-
body {
|
|
37
|
-
color: #b0b0b0;
|
|
38
|
-
background-color: #101010;
|
|
39
|
-
}
|
|
40
|
-
</style>`
|
|
41
|
-
);
|
|
42
|
-
server.close();
|
|
43
|
-
resolve({
|
|
44
|
-
base: urlPostfix,
|
|
45
|
-
data: params,
|
|
46
|
-
});
|
|
47
|
-
} catch (e) {
|
|
48
|
-
reject(e);
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
.listen(port, () => {
|
|
52
|
-
const options = browserName ? {app: {name: browserName }} : undefined
|
|
53
|
-
// open the browser to the authorize url to start the workflow
|
|
54
|
-
open(authorizeUrl, options).then((childProcess) => {
|
|
55
|
-
childProcess.unref();
|
|
56
|
-
clearTimeout(timeoutId);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const timeoutId = setTimeout(() => {
|
|
61
|
-
if (server.listening) {
|
|
62
|
-
try {
|
|
63
|
-
server.close();
|
|
64
|
-
} finally {
|
|
65
|
-
throw new Error(
|
|
66
|
-
'Authenticator timed out before authentication completed in the browser.'
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}, 59_000);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
module.exports = Authenticator;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
const Authenticator = require('@friggframework/devtools/test/Authenticator')
|
|
2
|
-
const { createMockIntegration, createMockApiObject } = require('mock-integration');
|
|
3
|
-
const { testAutherDefinition } = require('./auther-definition-tester');
|
|
4
|
-
const { testDefinitionRequiredAuthMethods } = require('./auther-definition-method-tester');
|
|
5
|
-
const { } = require('./../../utils/test-environment');
|
|
6
|
-
const {
|
|
7
|
-
TestMongo,
|
|
8
|
-
overrideEnvironment,
|
|
9
|
-
restoreEnvironment,
|
|
10
|
-
globalTeardown,
|
|
11
|
-
globalSetup,
|
|
12
|
-
} = require('./../../../utils/test-environment');
|
|
13
|
-
|
|
14
|
-
module.exports = {
|
|
15
|
-
createMockIntegration,
|
|
16
|
-
createMockApiObject,
|
|
17
|
-
testDefinitionRequiredAuthMethods,
|
|
18
|
-
testAutherDefinition,
|
|
19
|
-
Authenticator,
|
|
20
|
-
TestMongo,
|
|
21
|
-
overrideEnvironment,
|
|
22
|
-
restoreEnvironment,
|
|
23
|
-
globalTeardown,
|
|
24
|
-
globalSetup,
|
|
25
|
-
};
|
|
File without changes
|
|
File without changes
|