@anmiles/google-api-wrapper 17.0.9 → 18.0.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/.eslintignore +2 -0
- package/.eslintrc.js +23 -7
- package/.vscode/settings.json +1 -0
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +16 -16
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +9 -7
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -3
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +1 -1
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/paths.d.ts +5 -5
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +1 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/profiles.d.ts +4 -4
- package/dist/lib/profiles.d.ts.map +1 -1
- package/dist/lib/profiles.js +4 -4
- package/dist/lib/profiles.js.map +1 -1
- package/dist/lib/renderer.d.ts +2 -2
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +1 -1
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/secrets.d.ts +14 -13
- package/dist/lib/secrets.d.ts.map +1 -1
- package/dist/lib/secrets.js +13 -12
- package/dist/lib/secrets.js.map +1 -1
- package/dist/types/options.d.ts +9 -0
- package/dist/types/options.d.ts.map +1 -0
- package/dist/types/{common.js → options.js} +1 -1
- package/dist/types/options.js.map +1 -0
- package/dist/types/secrets.d.ts +2 -5
- package/dist/types/secrets.d.ts.map +1 -1
- package/jest.config.js +3 -11
- package/package.json +63 -56
- package/src/index.ts +2 -0
- package/src/lib/__tests__/api.test.ts +30 -24
- package/src/lib/__tests__/auth.test.ts +4 -5
- package/src/lib/__tests__/paths.test.ts +6 -5
- package/src/lib/__tests__/profiles.test.ts +17 -27
- package/src/lib/__tests__/renderer.test.ts +7 -7
- package/src/lib/__tests__/secrets.test.ts +54 -77
- package/src/lib/api.ts +36 -26
- package/src/lib/auth.ts +5 -5
- package/src/lib/paths.ts +8 -8
- package/src/lib/profiles.ts +6 -6
- package/src/lib/renderer.ts +8 -9
- package/src/lib/secrets.ts +22 -19
- package/src/types/options.ts +10 -0
- package/src/types/secrets.ts +9 -13
- package/tsconfig.build.json +2 -2
- package/tsconfig.json +5 -5
- package/tsconfig.test.json +3 -3
- package/.github/workflows/ci.yml +0 -94
- package/dist/types/common.d.ts +0 -4
- package/dist/types/common.d.ts.map +0 -1
- package/dist/types/common.js.map +0 -1
- package/dist/types/index.d.ts +0 -3
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -19
- package/dist/types/index.js.map +0 -1
- package/src/types/common.ts +0 -3
- package/src/types/index.ts +0 -2
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import http from 'http';
|
|
3
|
-
import path from 'path';
|
|
3
|
+
import type path from 'path';
|
|
4
4
|
import { open } from 'out-url';
|
|
5
5
|
import type GoogleApis from 'googleapis';
|
|
6
6
|
import logger from '@anmiles/logger';
|
|
7
7
|
import emitter from 'event-emitter';
|
|
8
|
-
import renderer from '../renderer';
|
|
9
|
-
import paths from '../paths';
|
|
10
|
-
import type { Secrets } from '../../types';
|
|
8
|
+
import type renderer from '../renderer';
|
|
9
|
+
import type paths from '../paths';
|
|
10
|
+
import type { Secrets } from '../../types/secrets';
|
|
11
11
|
import '@anmiles/prototypes';
|
|
12
12
|
|
|
13
13
|
import secrets from '../secrets';
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
const original = jest.requireActual<{ default : typeof secrets }>('../secrets').default;
|
|
15
16
|
jest.mock<typeof secrets>('../secrets', () => ({
|
|
16
17
|
getScopes : jest.fn().mockImplementation(() => scopesJSON),
|
|
17
18
|
getSecrets : jest.fn().mockImplementation(() => secretsJSON),
|
|
@@ -25,7 +26,7 @@ jest.mock<typeof secrets>('../secrets', () => ({
|
|
|
25
26
|
}));
|
|
26
27
|
|
|
27
28
|
jest.mock<Partial<typeof renderer>>('../renderer', () => ({
|
|
28
|
-
renderAuth : jest.fn().mockImplementation(({ profile, authUrl, scope } : { profile: string
|
|
29
|
+
renderAuth : jest.fn().mockImplementation(({ profile, authUrl, scope } : { profile : string; authUrl : string; scope : string[] }) => `content = profile = ${profile} authUrl = ${authUrl} scope = ${scope.join('|')}`),
|
|
29
30
|
renderDone : jest.fn().mockImplementation(() => 'content = done'),
|
|
30
31
|
}));
|
|
31
32
|
|
|
@@ -33,10 +34,10 @@ jest.mock<Partial<typeof http>>('http', () => ({
|
|
|
33
34
|
createServer : jest.fn().mockImplementation(() => server),
|
|
34
35
|
}));
|
|
35
36
|
|
|
36
|
-
let server: http.Server
|
|
37
|
+
let server: http.Server;
|
|
37
38
|
let response: http.ServerResponse;
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
function makeRequest(url: string | undefined): void {
|
|
40
41
|
server.emit('request', {
|
|
41
42
|
url,
|
|
42
43
|
headers : {
|
|
@@ -51,11 +52,13 @@ jest.mock<Partial<typeof fs>>('fs', () => ({
|
|
|
51
52
|
}));
|
|
52
53
|
|
|
53
54
|
jest.mock<Partial<typeof path>>('path', () => ({
|
|
54
|
-
join : jest.fn().mockImplementation((...
|
|
55
|
+
join : jest.fn().mockImplementation((...paths: string[]) => paths.join('/')),
|
|
55
56
|
}));
|
|
56
57
|
|
|
57
|
-
jest.mock<{ open: typeof open }>('out-url', () => ({
|
|
58
|
-
open : jest.fn().mockImplementation(
|
|
58
|
+
jest.mock<{ open : typeof open }>('out-url', () => ({
|
|
59
|
+
open : jest.fn().mockImplementation((url: string) => {
|
|
60
|
+
makeRequest(url.replace('http://localhost:6006', ''));
|
|
61
|
+
}),
|
|
59
62
|
}));
|
|
60
63
|
|
|
61
64
|
jest.mock<Partial<typeof logger>>('@anmiles/logger', () => ({
|
|
@@ -92,7 +95,6 @@ const scopesJSON: string[] = [
|
|
|
92
95
|
|
|
93
96
|
const secretsJSON: Secrets = {
|
|
94
97
|
web : {
|
|
95
|
-
/* eslint-disable camelcase */
|
|
96
98
|
client_id : 'client_id.apps.googleusercontent.com',
|
|
97
99
|
project_id : 'project_id',
|
|
98
100
|
auth_uri : 'https://accounts.google.com/o/oauth2/auth',
|
|
@@ -100,16 +102,14 @@ const secretsJSON: Secrets = {
|
|
|
100
102
|
auth_provider_x509_cert_url : 'https://www.googleapis.com/oauth2/v1/certs',
|
|
101
103
|
client_secret : 'client_secret',
|
|
102
104
|
redirect_uris : [ callbackURI ],
|
|
103
|
-
/* eslint-enable camelcase */
|
|
104
105
|
},
|
|
105
106
|
};
|
|
106
107
|
|
|
107
108
|
const credentialsJSON: GoogleApis.Auth.Credentials = {
|
|
108
|
-
// eslint-disable-next-line camelcase
|
|
109
109
|
access_token : 'access_token222',
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
let json:
|
|
112
|
+
let json: unknown;
|
|
113
113
|
|
|
114
114
|
const code = 'code';
|
|
115
115
|
const authUrl = 'https://authUrl';
|
|
@@ -120,26 +120,14 @@ const auth = {
|
|
|
120
120
|
|
|
121
121
|
let exists: boolean;
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
beforeAll(() => {
|
|
128
|
-
getJSONSpy = jest.spyOn(fs, 'getJSON');
|
|
129
|
-
getJSONAsyncSpy = jest.spyOn(fs, 'getJSONAsync');
|
|
130
|
-
readJSONSpy = jest.spyOn(fs, 'readJSON');
|
|
131
|
-
});
|
|
123
|
+
const getJSONSpy = jest.spyOn(fs, 'getJSON').mockReturnValue(json);
|
|
124
|
+
const getJSONAsyncSpy = jest.spyOn(fs, 'getJSONAsync').mockResolvedValue(json);
|
|
125
|
+
const readJSONSpy = jest.spyOn(fs, 'readJSON').mockReturnValue(json);
|
|
132
126
|
|
|
133
127
|
beforeEach(() => {
|
|
134
|
-
getJSONSpy.
|
|
135
|
-
getJSONAsyncSpy.
|
|
136
|
-
readJSONSpy.
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
afterAll(() => {
|
|
140
|
-
getJSONSpy.mockRestore();
|
|
141
|
-
getJSONAsyncSpy.mockRestore();
|
|
142
|
-
readJSONSpy.mockRestore();
|
|
128
|
+
getJSONSpy.mockReturnValue(json);
|
|
129
|
+
getJSONAsyncSpy.mockResolvedValue(json);
|
|
130
|
+
readJSONSpy.mockReturnValue(json);
|
|
143
131
|
});
|
|
144
132
|
|
|
145
133
|
describe('src/lib/secrets', () => {
|
|
@@ -152,13 +140,13 @@ describe('src/lib/secrets', () => {
|
|
|
152
140
|
original.getScopes();
|
|
153
141
|
|
|
154
142
|
expect(getJSONSpy).toHaveBeenCalled();
|
|
155
|
-
expect(getJSONSpy.mock.calls[0][0]).toEqual(scopesFile);
|
|
143
|
+
expect(getJSONSpy.mock.calls[0]?.[0]).toEqual(scopesFile);
|
|
156
144
|
});
|
|
157
145
|
|
|
158
146
|
it('should fallback to error', () => {
|
|
159
147
|
original.getScopes();
|
|
160
148
|
|
|
161
|
-
expect(getJSONSpy.mock.calls[0][1]).toThrow(scopesError);
|
|
149
|
+
expect(getJSONSpy.mock.calls[0]?.[1]).toThrow(scopesError);
|
|
162
150
|
});
|
|
163
151
|
|
|
164
152
|
it('should return scopes', () => {
|
|
@@ -177,13 +165,13 @@ describe('src/lib/secrets', () => {
|
|
|
177
165
|
original.getSecrets(profile);
|
|
178
166
|
|
|
179
167
|
expect(getJSONSpy).toHaveBeenCalled();
|
|
180
|
-
expect(getJSONSpy.mock.calls[0][0]).toEqual(secretsFile);
|
|
168
|
+
expect(getJSONSpy.mock.calls[0]?.[0]).toEqual(secretsFile);
|
|
181
169
|
});
|
|
182
170
|
|
|
183
171
|
it('should fallback to error', () => {
|
|
184
172
|
original.getSecrets(profile);
|
|
185
173
|
|
|
186
|
-
expect(getJSONSpy.mock.calls[0][1]).toThrow(secretsError);
|
|
174
|
+
expect(getJSONSpy.mock.calls[0]?.[1]).toThrow(secretsError);
|
|
187
175
|
});
|
|
188
176
|
|
|
189
177
|
it('should check secrets', () => {
|
|
@@ -209,14 +197,14 @@ describe('src/lib/secrets', () => {
|
|
|
209
197
|
await original.getCredentials(profile, auth);
|
|
210
198
|
|
|
211
199
|
expect(getJSONAsyncSpy).toHaveBeenCalled();
|
|
212
|
-
expect(getJSONAsyncSpy.mock.calls[0][0]).toEqual(credentialsFile);
|
|
200
|
+
expect(getJSONAsyncSpy.mock.calls[0]?.[0]).toEqual(credentialsFile);
|
|
213
201
|
});
|
|
214
202
|
|
|
215
203
|
it('should get json from credentials file if temporariness not set', async () => {
|
|
216
204
|
await original.getCredentials(profile, auth, { temporary : false });
|
|
217
205
|
|
|
218
206
|
expect(getJSONAsyncSpy).toHaveBeenCalled();
|
|
219
|
-
expect(getJSONAsyncSpy.mock.calls[0][0]).toEqual(credentialsFile);
|
|
207
|
+
expect(getJSONAsyncSpy.mock.calls[0]?.[0]).toEqual(credentialsFile);
|
|
220
208
|
});
|
|
221
209
|
|
|
222
210
|
it('should not get json from credentials file if temporariness set', async () => {
|
|
@@ -232,8 +220,8 @@ describe('src/lib/secrets', () => {
|
|
|
232
220
|
|
|
233
221
|
expect(secrets.createCredentials).not.toHaveBeenCalled();
|
|
234
222
|
|
|
235
|
-
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
236
|
-
const result = await fallback();
|
|
223
|
+
const fallback = getJSONAsyncSpy.mock.calls[0]?.[1];
|
|
224
|
+
const result = await fallback?.();
|
|
237
225
|
|
|
238
226
|
expect(readJSONSpy).not.toHaveBeenCalled();
|
|
239
227
|
expect(secrets.createCredentials).toHaveBeenCalledWith(profile, auth, undefined, 'consent');
|
|
@@ -247,8 +235,8 @@ describe('src/lib/secrets', () => {
|
|
|
247
235
|
|
|
248
236
|
expect(secrets.createCredentials).not.toHaveBeenCalled();
|
|
249
237
|
|
|
250
|
-
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
251
|
-
const result = await fallback();
|
|
238
|
+
const fallback = getJSONAsyncSpy.mock.calls[0]?.[1];
|
|
239
|
+
const result = await fallback?.();
|
|
252
240
|
|
|
253
241
|
expect(readJSONSpy).not.toHaveBeenCalled();
|
|
254
242
|
expect(secrets.createCredentials).toHaveBeenCalledWith(profile, auth, { temporary : false }, 'consent');
|
|
@@ -262,8 +250,8 @@ describe('src/lib/secrets', () => {
|
|
|
262
250
|
|
|
263
251
|
expect(secrets.createCredentials).not.toHaveBeenCalled();
|
|
264
252
|
|
|
265
|
-
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
266
|
-
const result = await fallback();
|
|
253
|
+
const fallback = getJSONAsyncSpy.mock.calls[0]?.[1];
|
|
254
|
+
const result = await fallback?.();
|
|
267
255
|
|
|
268
256
|
expect(readJSONSpy).toHaveBeenCalledWith(credentialsFile);
|
|
269
257
|
expect(secrets.createCredentials).toHaveBeenCalledWith(profile, auth, undefined, 'consent');
|
|
@@ -272,37 +260,31 @@ describe('src/lib/secrets', () => {
|
|
|
272
260
|
|
|
273
261
|
it('should call createCredentials without consent in fallback and replace refresh_token if existing credentials have refresh token', async () => {
|
|
274
262
|
exists = true;
|
|
275
|
-
// eslint-disable-next-line camelcase
|
|
276
263
|
readJSONSpy.mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token' });
|
|
277
264
|
|
|
278
265
|
await original.getCredentials(profile, auth);
|
|
279
266
|
|
|
280
267
|
expect(secrets.createCredentials).not.toHaveBeenCalled();
|
|
281
268
|
|
|
282
|
-
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
283
|
-
const result = await fallback();
|
|
269
|
+
const fallback = getJSONAsyncSpy.mock.calls[0]?.[1];
|
|
270
|
+
const result = await fallback?.();
|
|
284
271
|
|
|
285
272
|
expect(readJSONSpy).toHaveBeenCalledWith(credentialsFile);
|
|
286
|
-
expect(secrets.createCredentials).toHaveBeenCalledWith(profile, auth, undefined, undefined);
|
|
287
|
-
// eslint-disable-next-line camelcase
|
|
288
273
|
expect(result).toEqual({ ... credentialsJSON, refresh_token : 'refresh_token' });
|
|
289
274
|
});
|
|
290
275
|
|
|
291
276
|
it('should call createCredentials without consent in fallback and leave refresh token if existing credentials have refresh token', async () => {
|
|
292
277
|
exists = true;
|
|
293
|
-
// eslint-disable-next-line camelcase
|
|
294
278
|
readJSONSpy.mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token' });
|
|
295
|
-
// eslint-disable-next-line camelcase
|
|
296
279
|
jest.spyOn(secrets, 'createCredentials').mockResolvedValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token_exists' });
|
|
297
280
|
|
|
298
281
|
await original.getCredentials(profile, auth);
|
|
299
282
|
|
|
300
|
-
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
301
|
-
const result = await fallback();
|
|
283
|
+
const fallback = getJSONAsyncSpy.mock.calls[0]?.[1];
|
|
284
|
+
const result = await fallback?.();
|
|
302
285
|
|
|
303
286
|
expect(readJSONSpy).toHaveBeenCalledWith(credentialsFile);
|
|
304
287
|
expect(secrets.createCredentials).toHaveBeenCalledWith(profile, auth, undefined, undefined);
|
|
305
|
-
// eslint-disable-next-line camelcase
|
|
306
288
|
expect(result).toEqual({ ...credentialsJSON, refresh_token : 'refresh_token_exists' });
|
|
307
289
|
});
|
|
308
290
|
});
|
|
@@ -313,26 +295,22 @@ describe('src/lib/secrets', () => {
|
|
|
313
295
|
});
|
|
314
296
|
|
|
315
297
|
it('should return false if no refresh token', async () => {
|
|
316
|
-
// eslint-disable-next-line camelcase
|
|
317
298
|
expect(await original.validateCredentials({ access_token : 'token' })).toEqual({ isValid : false, validationError : 'Credentials does not have refresh_token' });
|
|
318
299
|
});
|
|
319
300
|
|
|
320
301
|
it('should return false if no expiration date', async () => {
|
|
321
|
-
// eslint-disable-next-line camelcase
|
|
322
302
|
expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token' })).toEqual({ isValid : false, validationError : 'Credentials does not have expiry_date' });
|
|
323
303
|
});
|
|
324
304
|
|
|
325
305
|
it('should return true if credentials are not more than 1 week ago', async () => {
|
|
326
306
|
const expiryDate = new Date();
|
|
327
307
|
expiryDate.setDate(expiryDate.getDate() - 6);
|
|
328
|
-
// eslint-disable-next-line camelcase
|
|
329
308
|
expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token', expiry_date : expiryDate.getTime() })).toEqual({ isValid : true });
|
|
330
309
|
});
|
|
331
310
|
|
|
332
311
|
it('should return false if credentials are more than 1 week ago', async () => {
|
|
333
312
|
const expiryDate = new Date();
|
|
334
313
|
expiryDate.setDate(expiryDate.getDate() - 8);
|
|
335
|
-
// eslint-disable-next-line camelcase
|
|
336
314
|
expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token', expiry_date : expiryDate.getTime() })).toEqual({ isValid : false, validationError : 'Credentials expired' });
|
|
337
315
|
});
|
|
338
316
|
});
|
|
@@ -370,11 +348,10 @@ describe('src/lib/secrets', () => {
|
|
|
370
348
|
});
|
|
371
349
|
|
|
372
350
|
it('should generate authUrl', async () => {
|
|
373
|
-
original.createCredentials(profile, auth);
|
|
351
|
+
void original.createCredentials(profile, auth);
|
|
374
352
|
await Promise.resolve();
|
|
375
353
|
|
|
376
354
|
expect(auth.generateAuthUrl).toHaveBeenCalledWith({
|
|
377
|
-
// eslint-disable-next-line camelcase
|
|
378
355
|
access_type : 'offline',
|
|
379
356
|
prompt : undefined,
|
|
380
357
|
scope : [
|
|
@@ -385,11 +362,10 @@ describe('src/lib/secrets', () => {
|
|
|
385
362
|
});
|
|
386
363
|
|
|
387
364
|
it('should generate authUrl and require consent if explicitly asked', async () => {
|
|
388
|
-
original.createCredentials(profile, auth, { temporary : true }, 'consent');
|
|
365
|
+
void original.createCredentials(profile, auth, { temporary : true }, 'consent');
|
|
389
366
|
await Promise.resolve();
|
|
390
367
|
|
|
391
368
|
expect(auth.generateAuthUrl).toHaveBeenCalledWith({
|
|
392
|
-
// eslint-disable-next-line camelcase
|
|
393
369
|
access_type : 'offline',
|
|
394
370
|
prompt : 'consent',
|
|
395
371
|
scope : [
|
|
@@ -400,11 +376,10 @@ describe('src/lib/secrets', () => {
|
|
|
400
376
|
});
|
|
401
377
|
|
|
402
378
|
it('should generate authUrl with custom scopes', async () => {
|
|
403
|
-
original.createCredentials(profile, auth, { scopes : [ 'scope1', 'scope2' ] });
|
|
379
|
+
void original.createCredentials(profile, auth, { scopes : [ 'scope1', 'scope2' ] });
|
|
404
380
|
await Promise.resolve();
|
|
405
381
|
|
|
406
382
|
expect(auth.generateAuthUrl).toHaveBeenCalledWith({
|
|
407
|
-
// eslint-disable-next-line camelcase
|
|
408
383
|
access_type : 'offline',
|
|
409
384
|
prompt : undefined,
|
|
410
385
|
scope : [ 'scope1', 'scope2' ],
|
|
@@ -412,7 +387,7 @@ describe('src/lib/secrets', () => {
|
|
|
412
387
|
});
|
|
413
388
|
|
|
414
389
|
it('should create server on 6006 port', async () => {
|
|
415
|
-
original.createCredentials(profile, auth);
|
|
390
|
+
void original.createCredentials(profile, auth);
|
|
416
391
|
await Promise.resolve();
|
|
417
392
|
|
|
418
393
|
expect(http.createServer).toHaveBeenCalled();
|
|
@@ -420,7 +395,7 @@ describe('src/lib/secrets', () => {
|
|
|
420
395
|
});
|
|
421
396
|
|
|
422
397
|
it('should open browser page and warn about it once listening', async () => {
|
|
423
|
-
original.createCredentials(profile, auth);
|
|
398
|
+
void original.createCredentials(profile, auth);
|
|
424
399
|
await Promise.resolve();
|
|
425
400
|
|
|
426
401
|
server.emit('listening');
|
|
@@ -430,7 +405,7 @@ describe('src/lib/secrets', () => {
|
|
|
430
405
|
});
|
|
431
406
|
|
|
432
407
|
it('should not open browser page and warn about it until listening', async () => {
|
|
433
|
-
original.createCredentials(profile, auth);
|
|
408
|
+
void original.createCredentials(profile, auth);
|
|
434
409
|
await Promise.resolve();
|
|
435
410
|
|
|
436
411
|
expect(open).not.toHaveBeenCalled();
|
|
@@ -438,7 +413,7 @@ describe('src/lib/secrets', () => {
|
|
|
438
413
|
});
|
|
439
414
|
|
|
440
415
|
it('should show nothing on the browser page if request.url is empty', async () => {
|
|
441
|
-
original.createCredentials(profile, auth);
|
|
416
|
+
void original.createCredentials(profile, auth);
|
|
442
417
|
makeRequest('');
|
|
443
418
|
await Promise.resolve();
|
|
444
419
|
|
|
@@ -446,7 +421,7 @@ describe('src/lib/secrets', () => {
|
|
|
446
421
|
});
|
|
447
422
|
|
|
448
423
|
it('should show opening instructions if opened the home page', async () => {
|
|
449
|
-
original.createCredentials(profile, auth);
|
|
424
|
+
void original.createCredentials(profile, auth);
|
|
450
425
|
makeRequest('/');
|
|
451
426
|
await Promise.resolve();
|
|
452
427
|
|
|
@@ -454,7 +429,7 @@ describe('src/lib/secrets', () => {
|
|
|
454
429
|
});
|
|
455
430
|
|
|
456
431
|
it('should ask to close webpage', async () => {
|
|
457
|
-
original.createCredentials(profile, auth);
|
|
432
|
+
void original.createCredentials(profile, auth);
|
|
458
433
|
makeRequest(tokenUrl);
|
|
459
434
|
await Promise.resolve();
|
|
460
435
|
|
|
@@ -462,13 +437,15 @@ describe('src/lib/secrets', () => {
|
|
|
462
437
|
});
|
|
463
438
|
|
|
464
439
|
it('should close server and destroy all connections if request.url is truthy', async () => {
|
|
465
|
-
original.createCredentials(profile, auth);
|
|
440
|
+
void original.createCredentials(profile, auth);
|
|
466
441
|
makeRequest(tokenUrl);
|
|
467
442
|
await Promise.resolve();
|
|
468
443
|
|
|
469
444
|
expect(server.close).toHaveBeenCalled();
|
|
470
445
|
|
|
471
|
-
connections.forEach((connection) =>
|
|
446
|
+
connections.forEach((connection) => {
|
|
447
|
+
expect(connection.destroy).toHaveBeenCalled();
|
|
448
|
+
});
|
|
472
449
|
});
|
|
473
450
|
|
|
474
451
|
it('should close server and resolve if request.url is truthy', async () => {
|
|
@@ -480,7 +457,7 @@ describe('src/lib/secrets', () => {
|
|
|
480
457
|
});
|
|
481
458
|
|
|
482
459
|
it('should not close server if request.url is falsy', async () => {
|
|
483
|
-
original.createCredentials(profile, auth);
|
|
460
|
+
void original.createCredentials(profile, auth);
|
|
484
461
|
makeRequest(undefined);
|
|
485
462
|
await Promise.resolve();
|
|
486
463
|
|
|
@@ -490,14 +467,14 @@ describe('src/lib/secrets', () => {
|
|
|
490
467
|
it('should re-throw a server error if error is not EADDRINUSE', () => {
|
|
491
468
|
const error = { code : 'RANDOM', message : 'random error' } as NodeJS.ErrnoException;
|
|
492
469
|
|
|
493
|
-
original.createCredentials(profile, auth);
|
|
470
|
+
void original.createCredentials(profile, auth);
|
|
494
471
|
expect(() => server.emit('error', error)).toThrow(error.message);
|
|
495
472
|
});
|
|
496
473
|
|
|
497
474
|
it('should not re-throw a server error and try to listen again in 1000 seconds if error is EADDRINUSE', () => {
|
|
498
475
|
const error = { code : 'EADDRINUSE' } as NodeJS.ErrnoException;
|
|
499
476
|
|
|
500
|
-
original.createCredentials(profile, auth);
|
|
477
|
+
void original.createCredentials(profile, auth);
|
|
501
478
|
expect(server.listen).toHaveBeenCalledTimes(1);
|
|
502
479
|
expect(() => server.emit('error', error)).not.toThrow();
|
|
503
480
|
expect(server.listen).toHaveBeenCalledTimes(1);
|
|
@@ -538,7 +515,7 @@ describe('src/lib/secrets', () => {
|
|
|
538
515
|
it('should output error if redirect_uri is incorrect', () => {
|
|
539
516
|
const wrongSecretsJSON = { ...secretsJSON };
|
|
540
517
|
wrongSecretsJSON.web.redirect_uris[0] = wrongRedirectURI;
|
|
541
|
-
const func = () => original.checkSecrets(profile, wrongSecretsJSON, secretsFile);
|
|
518
|
+
const func = (): true => original.checkSecrets(profile, wrongSecretsJSON, secretsFile);
|
|
542
519
|
|
|
543
520
|
expect(func).toThrow('Error in credentials file: redirect URI should be http://localhost:6006/oauthcallback.\nsecretsError');
|
|
544
521
|
});
|
package/src/lib/api.ts
CHANGED
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
import type GoogleApis from 'googleapis';
|
|
2
2
|
import { log, warn } from '@anmiles/logger';
|
|
3
3
|
import sleep from '@anmiles/sleep';
|
|
4
|
-
import type { AuthOptions, CommonOptions } from '../types';
|
|
4
|
+
import type { AuthOptions, CommonOptions } from '../types/options';
|
|
5
5
|
import { getAuth } from './auth';
|
|
6
6
|
import { deleteCredentials } from './secrets';
|
|
7
|
+
import '@anmiles/prototypes';
|
|
7
8
|
|
|
8
9
|
const requestInterval = 300;
|
|
9
10
|
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
(params?: { pageToken: string | undefined }, options?: GoogleApis.Common.MethodOptions): Promise<GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>>>;
|
|
13
|
-
(callback: (err: Error | null, res?: GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>> | null) => void): void;
|
|
14
|
-
}
|
|
11
|
+
type ListParams = Record<string, unknown> & {
|
|
12
|
+
pageToken : string | undefined;
|
|
15
13
|
};
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
interface CommonAPI<TItem> {
|
|
16
|
+
list : (
|
|
17
|
+
params?: ListParams,
|
|
18
|
+
options?: GoogleApis.Common.MethodOptions
|
|
19
|
+
) => Promise<GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CommonResponse<TItem> {
|
|
23
|
+
items? : TItem[];
|
|
19
24
|
pageInfo?: {
|
|
20
|
-
totalResults
|
|
21
|
-
}
|
|
22
|
-
nextPageToken
|
|
23
|
-
}
|
|
25
|
+
totalResults? : number | null | undefined;
|
|
26
|
+
};
|
|
27
|
+
nextPageToken? : string | null | undefined;
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
class API<TGoogleAPI> {
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
api : TGoogleAPI | undefined;
|
|
32
|
+
private auth : GoogleApis.Common.OAuth2Client | undefined;
|
|
28
33
|
|
|
29
34
|
constructor(
|
|
30
|
-
private getter: (auth: GoogleApis.Common.OAuth2Client) => TGoogleAPI,
|
|
31
|
-
private profile: string,
|
|
32
|
-
private authOptions?: AuthOptions,
|
|
35
|
+
private readonly getter: (auth: GoogleApis.Common.OAuth2Client) => TGoogleAPI,
|
|
36
|
+
private readonly profile: string,
|
|
37
|
+
private readonly authOptions?: AuthOptions,
|
|
33
38
|
) { }
|
|
34
39
|
|
|
35
|
-
async init() {
|
|
40
|
+
async init(): Promise<void> {
|
|
36
41
|
this.auth = await getAuth(this.profile, this.authOptions);
|
|
37
42
|
this.api = this.getter(this.auth);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
async getItems<TItem>(selectAPI: (api: TGoogleAPI) => CommonAPI<TItem>, params:
|
|
45
|
+
async getItems<TItem>(selectAPI: (api: TGoogleAPI) => CommonAPI<TItem>, params: object, options?: CommonOptions): Promise<TItem[]> {
|
|
41
46
|
const items: TItem[] = [];
|
|
42
47
|
|
|
43
48
|
let pageToken: string | null | undefined = undefined;
|
|
@@ -47,14 +52,14 @@ class API<TGoogleAPI> {
|
|
|
47
52
|
|
|
48
53
|
try {
|
|
49
54
|
if (!this.api) {
|
|
50
|
-
throw 'API is not initialized. Call `init` before getting items.';
|
|
55
|
+
throw new Error('API is not initialized. Call `init` before getting items.');
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
const selectedAPI = selectAPI(this.api);
|
|
54
59
|
|
|
55
60
|
response = await selectedAPI.list({ ...params, pageToken });
|
|
56
61
|
} catch (ex) {
|
|
57
|
-
const message =
|
|
62
|
+
const { message } = Error.parse(ex);
|
|
58
63
|
|
|
59
64
|
if ((message === 'invalid_grant' || message === 'Invalid credentials') && !this.authOptions?.temporary) {
|
|
60
65
|
warn('Access token stored is invalid, re-creating...');
|
|
@@ -69,7 +74,7 @@ class API<TGoogleAPI> {
|
|
|
69
74
|
response.data.items?.forEach((item) => items.push(item));
|
|
70
75
|
|
|
71
76
|
if (!options?.hideProgress) {
|
|
72
|
-
log(`Getting items (${items.length} of ${response.data.pageInfo?.totalResults
|
|
77
|
+
log(`Getting items (${items.length} of ${response.data.pageInfo?.totalResults ?? 'many'})...`);
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
await sleep(requestInterval);
|
|
@@ -80,12 +85,16 @@ class API<TGoogleAPI> {
|
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
async function getAPI<TGoogleAPI>(
|
|
88
|
+
async function getAPI<TGoogleAPI>(
|
|
89
|
+
getter: (auth: GoogleApis.Common.OAuth2Client) => TGoogleAPI,
|
|
90
|
+
profile: string,
|
|
91
|
+
authOptions?: AuthOptions,
|
|
92
|
+
): Promise<API<TGoogleAPI>> {
|
|
84
93
|
if (!authOptions?.temporary) {
|
|
85
|
-
const writableScopes = authOptions?.scopes?.filter((scope) => !scope.endsWith('.readonly'))
|
|
94
|
+
const writableScopes = authOptions?.scopes?.filter((scope) => !scope.endsWith('.readonly')) ?? [];
|
|
86
95
|
|
|
87
96
|
if (writableScopes.length > 0) {
|
|
88
|
-
warn(`WARNING: trying to create permanent credentials using non-readonly scopes (${writableScopes}). Permanent credentials will be stored in the file and potentially might be re-used to modify your data`);
|
|
97
|
+
warn(`WARNING: trying to create permanent credentials using non-readonly scopes (${writableScopes.join(', ')}). Permanent credentials will be stored in the file and potentially might be re-used to modify your data`);
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
100
|
|
|
@@ -94,5 +103,6 @@ async function getAPI<TGoogleAPI>(getter: (auth: GoogleApis.Common.OAuth2Client)
|
|
|
94
103
|
return instance;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
export { getAPI };
|
|
106
|
+
export { getAPI, API };
|
|
107
|
+
export type { CommonResponse };
|
|
98
108
|
export default { getAPI, API };
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { google } from 'googleapis';
|
|
2
2
|
import type GoogleApis from 'googleapis';
|
|
3
3
|
import { info, warn } from '@anmiles/logger';
|
|
4
|
-
import type { CommonOptions, AuthOptions } from '../types';
|
|
4
|
+
import type { CommonOptions, AuthOptions } from '../types/options';
|
|
5
5
|
import { getProfiles } from './profiles';
|
|
6
6
|
import { getCredentials, getSecrets } from './secrets';
|
|
7
7
|
|
|
8
8
|
import auth from './auth';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
export default { login, getAuth };
|
|
12
|
-
|
|
13
|
-
async function login(profile?: string, options?: CommonOptions & AuthOptions): Promise<void> {
|
|
10
|
+
async function login(profile?: string, options?: AuthOptions & CommonOptions): Promise<void> {
|
|
14
11
|
const profiles = getProfiles().filter((p) => !profile || p === profile);
|
|
15
12
|
|
|
16
13
|
for (const profile of profiles) {
|
|
@@ -40,3 +37,6 @@ async function getAuth(profile: string, options?: AuthOptions): Promise<GoogleAp
|
|
|
40
37
|
google.options({ auth : googleAuth });
|
|
41
38
|
return googleAuth;
|
|
42
39
|
}
|
|
40
|
+
|
|
41
|
+
export { login, getAuth };
|
|
42
|
+
export default { login, getAuth };
|
package/src/lib/paths.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import type { templates } from './renderer';
|
|
3
3
|
|
|
4
|
-
export { getProfilesFile, getScopesFile, getSecretsFile, getCredentialsFile, getTemplateFile };
|
|
5
|
-
export default { getProfilesFile, getScopesFile, getSecretsFile, getCredentialsFile, getTemplateFile };
|
|
6
|
-
|
|
7
4
|
const dirPaths = {
|
|
8
5
|
input : 'input',
|
|
9
6
|
secrets : 'secrets',
|
|
@@ -11,22 +8,25 @@ const dirPaths = {
|
|
|
11
8
|
templates : 'node_modules/@anmiles/google-api-wrapper/dist/templates',
|
|
12
9
|
};
|
|
13
10
|
|
|
14
|
-
function getProfilesFile() {
|
|
11
|
+
function getProfilesFile(): string {
|
|
15
12
|
return path.join(dirPaths.input, 'profiles.json');
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
function getScopesFile() {
|
|
15
|
+
function getScopesFile(): string {
|
|
19
16
|
return 'scopes.json';
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
function getSecretsFile(profile: string) {
|
|
19
|
+
function getSecretsFile(profile: string): string {
|
|
23
20
|
return path.join(dirPaths.secrets, `${profile}.json`);
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
function getCredentialsFile(profile: string) {
|
|
23
|
+
function getCredentialsFile(profile: string): string {
|
|
27
24
|
return path.join(dirPaths.secrets, `${profile}.credentials.json`);
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
function getTemplateFile(templateName: keyof typeof templates) {
|
|
27
|
+
function getTemplateFile(templateName: keyof typeof templates): string {
|
|
31
28
|
return path.join(dirPaths.templates, `${templateName}.html`);
|
|
32
29
|
}
|
|
30
|
+
|
|
31
|
+
export { getProfilesFile, getScopesFile, getSecretsFile, getCredentialsFile, getTemplateFile };
|
|
32
|
+
export default { getProfilesFile, getScopesFile, getSecretsFile, getCredentialsFile, getTemplateFile };
|
package/src/lib/profiles.ts
CHANGED
|
@@ -4,9 +4,6 @@ import { getProfilesFile } from './paths';
|
|
|
4
4
|
|
|
5
5
|
import profiles from './profiles';
|
|
6
6
|
|
|
7
|
-
export { getProfiles, setProfiles, createProfile, filterProfiles };
|
|
8
|
-
export default { getProfiles, setProfiles, createProfile, filterProfiles };
|
|
9
|
-
|
|
10
7
|
function getProfiles(): string[] {
|
|
11
8
|
const profilesFile = getProfilesFile();
|
|
12
9
|
return fs.getJSON(profilesFile, () => []);
|
|
@@ -19,7 +16,7 @@ function setProfiles(profiles: string[]): void {
|
|
|
19
16
|
|
|
20
17
|
function createProfile(profile?: string): void {
|
|
21
18
|
if (!profile) {
|
|
22
|
-
throw 'Usage: `npm run create <profile>` where `profile` - is any profile name you want';
|
|
19
|
+
throw new Error('Usage: `npm run create <profile>` where `profile` - is any profile name you want');
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
const existingProfiles = profiles.getProfiles();
|
|
@@ -36,7 +33,7 @@ function filterProfiles(profile?: string): string[] {
|
|
|
36
33
|
const existingProfiles = profiles.getProfiles();
|
|
37
34
|
|
|
38
35
|
if (existingProfiles.length === 0) {
|
|
39
|
-
throw 'Please `npm run create` at least one profile';
|
|
36
|
+
throw new Error('Please `npm run create` at least one profile');
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
if (!profile) {
|
|
@@ -47,5 +44,8 @@ function filterProfiles(profile?: string): string[] {
|
|
|
47
44
|
return [ profile ];
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
throw `Profile '${profile}' does not exist
|
|
47
|
+
throw new Error(`Profile '${profile}' does not exist`);
|
|
51
48
|
}
|
|
49
|
+
|
|
50
|
+
export { getProfiles, setProfiles, createProfile, filterProfiles };
|
|
51
|
+
export default { getProfiles, setProfiles, createProfile, filterProfiles };
|