@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.
@@ -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, IntegrationModel, createObjectId } = require('@friggframework/core')
1
+ const { Auther, Credential, Entity, IntegrationFactory, createObjectId } = require('@friggframework/core');
2
+
2
3
 
3
4
  async function createMockIntegration(IntegrationClassDef, userId = null, config = {},) {
4
- const integration = new IntegrationClassDef();
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
- integration.record = await IntegrationModel.create({
45
- entities,
46
- user: userId,
47
- config: {type: IntegrationClassDef.Config.name, ...config}
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
 
@@ -1,3 +0,0 @@
1
- # migrations
2
-
3
- This package exports the `migrations` class and functions used in [Frigg](https://friggframework.org). You can find its documentation [on Frigg's website](https://docs.friggframework.org/packages/migrations).
File without changes
@@ -1,3 +0,0 @@
1
- module.exports = async () => {
2
- preset: "@friggframework/test-environment";
3
- };
@@ -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
- };