@anmiles/google-api-wrapper 18.0.3 → 19.0.1
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/.nycrc.json +27 -0
- package/CHANGELOG.md +32 -21
- package/README.md +1 -1
- package/cspell.json +22 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +5 -11
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +5 -6
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +2 -9
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +3 -23
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/credentials/generator.d.ts +4 -0
- package/dist/lib/credentials/generator.d.ts.map +1 -0
- package/dist/lib/credentials/generator.js +60 -0
- package/dist/lib/credentials/generator.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.d.ts.map +1 -0
- package/dist/lib/credentials/index.js +42 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/validator.d.ts +7 -0
- package/dist/lib/credentials/validator.d.ts.map +1 -0
- package/dist/lib/credentials/validator.js +20 -0
- package/dist/lib/credentials/validator.js.map +1 -0
- package/dist/lib/login.d.ts +3 -0
- package/dist/lib/login.d.ts.map +1 -0
- package/dist/lib/login.js +19 -0
- package/dist/lib/login.js.map +1 -0
- package/dist/lib/profiles.d.ts +4 -12
- package/dist/lib/profiles.d.ts.map +1 -1
- package/dist/lib/profiles.js +8 -11
- package/dist/lib/profiles.js.map +1 -1
- package/dist/lib/renderer.d.ts +7 -25
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +26 -23
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/scopes.d.ts +2 -0
- package/dist/lib/scopes.d.ts.map +1 -0
- package/dist/lib/scopes.js +22 -0
- package/dist/lib/scopes.js.map +1 -0
- package/dist/lib/secrets.d.ts +2 -28
- package/dist/lib/secrets.d.ts.map +1 -1
- package/dist/lib/secrets.js +15 -115
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/utils/paths.d.ts +7 -0
- package/dist/lib/utils/paths.d.ts.map +1 -0
- package/dist/lib/{paths.js → utils/paths.js} +6 -8
- package/dist/lib/utils/paths.js.map +1 -0
- package/dist/templates/auth.html +1 -1
- package/dist/templates/index.html +6 -1
- package/dist/templates/page.html +1 -2
- package/dist/templates/script.html +0 -0
- package/{src/templates/css.html → dist/templates/style.html} +2 -2
- package/dist/types/options.d.ts +2 -3
- package/dist/types/options.d.ts.map +1 -1
- package/dist/types/secrets.d.ts +1 -2
- package/dist/types/secrets.d.ts.map +1 -1
- package/eslint.config.mts +43 -0
- package/jest.config.js +9 -9
- package/package.json +40 -30
- package/src/index.ts +3 -2
- package/src/lib/__tests__/__snapshots__/renderer.test.ts.snap +273 -0
- package/src/lib/__tests__/__snapshots__/scopes.test.ts.snap +6 -0
- package/src/lib/__tests__/__snapshots__/secrets.test.ts.snap +38 -0
- package/src/lib/__tests__/api.test.ts +72 -74
- package/src/lib/__tests__/auth.test.ts +38 -114
- package/src/lib/__tests__/login.test.ts +71 -0
- package/src/lib/__tests__/profiles.test.ts +50 -93
- package/src/lib/__tests__/renderer.test.ts +16 -89
- package/src/lib/__tests__/scopes.test.ts +41 -0
- package/src/lib/__tests__/secrets.test.ts +47 -541
- package/src/lib/api.ts +19 -21
- package/src/lib/auth.ts +5 -25
- package/src/lib/credentials/__tests__/generator.test.ts +249 -0
- package/src/lib/credentials/__tests__/index.test.ts +213 -0
- package/src/lib/credentials/__tests__/validator.test.ts +43 -0
- package/src/lib/credentials/generator.ts +70 -0
- package/src/lib/credentials/index.ts +50 -0
- package/src/lib/credentials/validator.ts +29 -0
- package/src/lib/login.ts +22 -0
- package/src/lib/profiles.ts +9 -12
- package/src/lib/renderer.ts +32 -27
- package/src/lib/scopes.ts +18 -0
- package/src/lib/secrets.ts +21 -141
- package/src/lib/utils/paths.ts +30 -0
- package/src/templates/auth.html +1 -1
- package/src/templates/index.html +6 -1
- package/src/templates/page.html +1 -2
- package/src/templates/script.html +0 -0
- package/{dist/templates/css.html → src/templates/style.html} +2 -2
- package/src/types/options.ts +5 -7
- package/src/types/secrets.ts +8 -10
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +0 -5
- package/tsconfig.test.json +1 -1
- package/.eslintignore +0 -2
- package/.eslintrc.js +0 -30
- package/.vscode/settings.json +0 -9
- package/coverage.config.js +0 -8
- package/dist/lib/paths.d.ts +0 -16
- package/dist/lib/paths.d.ts.map +0 -1
- package/dist/lib/paths.js.map +0 -1
- package/src/lib/__tests__/paths.test.ts +0 -77
- package/src/lib/paths.ts +0 -32
package/src/lib/api.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import type GoogleApis from 'googleapis';
|
|
2
1
|
import { log, warn } from '@anmiles/logger';
|
|
2
|
+
import '@anmiles/prototypes';
|
|
3
3
|
import sleep from '@anmiles/sleep';
|
|
4
|
+
import type GoogleApis from 'googleapis';
|
|
5
|
+
|
|
4
6
|
import type { AuthOptions, CommonOptions } from '../types/options';
|
|
7
|
+
|
|
5
8
|
import { getAuth } from './auth';
|
|
6
|
-
import { deleteCredentials } from './
|
|
7
|
-
import '@anmiles/prototypes';
|
|
9
|
+
import { deleteCredentials } from './credentials';
|
|
8
10
|
|
|
9
11
|
const requestInterval = 300;
|
|
10
12
|
|
|
11
13
|
type ListParams = Record<string, unknown> & {
|
|
12
|
-
pageToken
|
|
14
|
+
pageToken: string | undefined;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
interface CommonAPI<TItem> {
|
|
@@ -17,27 +19,27 @@ interface CommonAPI<TItem> {
|
|
|
17
19
|
(
|
|
18
20
|
params?: ListParams,
|
|
19
21
|
options?: GoogleApis.Common.MethodOptions
|
|
20
|
-
)
|
|
22
|
+
): Promise<GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>>>;
|
|
21
23
|
(
|
|
22
|
-
callback: (err: Error | null, res?: GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>> | null)
|
|
24
|
+
callback: (err: Error | null, res?: GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>> | null)=> void
|
|
23
25
|
): void;
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
interface CommonResponse<TItem> {
|
|
28
|
-
items
|
|
29
|
+
export interface CommonResponse<TItem> {
|
|
30
|
+
items?: TItem[];
|
|
29
31
|
pageInfo?: {
|
|
30
|
-
totalResults
|
|
32
|
+
totalResults?: number | null | undefined;
|
|
31
33
|
};
|
|
32
|
-
nextPageToken
|
|
34
|
+
nextPageToken?: string | null | undefined;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
class API<TGoogleAPI> {
|
|
36
|
-
api
|
|
37
|
-
private auth
|
|
37
|
+
export class API<TGoogleAPI> {
|
|
38
|
+
api: TGoogleAPI | undefined;
|
|
39
|
+
private auth: GoogleApis.Common.OAuth2Client | undefined;
|
|
38
40
|
|
|
39
41
|
constructor(
|
|
40
|
-
private readonly getter: (auth: GoogleApis.Common.OAuth2Client)
|
|
42
|
+
private readonly getter: (auth: GoogleApis.Common.OAuth2Client)=> TGoogleAPI,
|
|
41
43
|
private readonly profile: string,
|
|
42
44
|
private readonly authOptions?: AuthOptions,
|
|
43
45
|
) { }
|
|
@@ -47,7 +49,7 @@ class API<TGoogleAPI> {
|
|
|
47
49
|
this.api = this.getter(this.auth);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
async getItems<TItem>(selectAPI: (api: TGoogleAPI)
|
|
52
|
+
async getItems<TItem>(selectAPI: (api: TGoogleAPI)=> CommonAPI<TItem>, params: object, options?: CommonOptions): Promise<TItem[]> {
|
|
51
53
|
const items: TItem[] = [];
|
|
52
54
|
|
|
53
55
|
let pageToken: string | null | undefined = undefined;
|
|
@@ -90,8 +92,8 @@ class API<TGoogleAPI> {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
async function getAPI<TGoogleAPI>(
|
|
94
|
-
getter: (auth: GoogleApis.Common.OAuth2Client)
|
|
95
|
+
export async function getAPI<TGoogleAPI>(
|
|
96
|
+
getter: (auth: GoogleApis.Common.OAuth2Client)=> TGoogleAPI,
|
|
95
97
|
profile: string,
|
|
96
98
|
authOptions?: AuthOptions,
|
|
97
99
|
): Promise<API<TGoogleAPI>> {
|
|
@@ -107,7 +109,3 @@ async function getAPI<TGoogleAPI>(
|
|
|
107
109
|
await instance.init();
|
|
108
110
|
return instance;
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
export { getAPI, API };
|
|
112
|
-
export type { CommonResponse };
|
|
113
|
-
export default { getAPI, API };
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
import { google } from 'googleapis';
|
|
2
2
|
import type GoogleApis from 'googleapis';
|
|
3
|
-
import { info, warn } from '@anmiles/logger';
|
|
4
|
-
import type { CommonOptions, AuthOptions } from '../types/options';
|
|
5
|
-
import { getProfiles } from './profiles';
|
|
6
|
-
import { getCredentials, getSecrets } from './secrets';
|
|
7
3
|
|
|
8
|
-
import
|
|
4
|
+
import type { AuthOptions } from '../types/options';
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import { getCredentials } from './credentials';
|
|
7
|
+
import { getSecrets } from './secrets';
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
if (!options?.hideProgress) {
|
|
15
|
-
warn(`${profile} - logging in...`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
await auth.getAuth(profile, options);
|
|
19
|
-
|
|
20
|
-
if (!options?.hideProgress) {
|
|
21
|
-
info(`${profile} - logged in successfully`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function getAuth(profile: string, options?: AuthOptions): Promise<GoogleApis.Common.OAuth2Client> {
|
|
9
|
+
export async function getAuth(profile: string, options?: AuthOptions): Promise<GoogleApis.Common.OAuth2Client> {
|
|
27
10
|
const secrets = getSecrets(profile);
|
|
28
11
|
|
|
29
12
|
const googleAuth = new google.auth.OAuth2(
|
|
@@ -34,9 +17,6 @@ async function getAuth(profile: string, options?: AuthOptions): Promise<GoogleAp
|
|
|
34
17
|
|
|
35
18
|
const tokens = await getCredentials(profile, googleAuth, options);
|
|
36
19
|
googleAuth.setCredentials(tokens);
|
|
37
|
-
google.options({ auth
|
|
20
|
+
google.options({ auth: googleAuth });
|
|
38
21
|
return googleAuth;
|
|
39
22
|
}
|
|
40
|
-
|
|
41
|
-
export { login, getAuth };
|
|
42
|
-
export default { login, getAuth };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
|
|
4
|
+
import logger from '@anmiles/logger';
|
|
5
|
+
import '@anmiles/prototypes';
|
|
6
|
+
import type GoogleApis from 'googleapis';
|
|
7
|
+
import mockFs from 'mock-fs';
|
|
8
|
+
import { open } from 'out-url';
|
|
9
|
+
|
|
10
|
+
import { renderAuth, renderDone } from '../../renderer';
|
|
11
|
+
import { getScopes } from '../../scopes';
|
|
12
|
+
import { getCredentialsFile } from '../../utils/paths';
|
|
13
|
+
import { generateCredentials } from '../generator';
|
|
14
|
+
|
|
15
|
+
jest.mock('http');
|
|
16
|
+
jest.mock('@anmiles/logger');
|
|
17
|
+
jest.mock('out-url');
|
|
18
|
+
jest.mock('../../renderer');
|
|
19
|
+
jest.mock('../../scopes');
|
|
20
|
+
|
|
21
|
+
jest.mock<Partial<typeof http>>('http', () => ({
|
|
22
|
+
createServer: jest.fn().mockImplementation(() => server),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
let server: http.Server;
|
|
26
|
+
let response: http.ServerResponse;
|
|
27
|
+
|
|
28
|
+
function makeRequest(url: string | undefined): void {
|
|
29
|
+
server.emit('request', { // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
30
|
+
url,
|
|
31
|
+
headers: {
|
|
32
|
+
host,
|
|
33
|
+
},
|
|
34
|
+
} as http.IncomingMessage, response);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const port = 6006;
|
|
38
|
+
const host = `localhost:${port}`;
|
|
39
|
+
|
|
40
|
+
const profile = 'username1';
|
|
41
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
42
|
+
|
|
43
|
+
const credentials: GoogleApis.Auth.Credentials = {
|
|
44
|
+
access_token: 'access_token_123',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const code = 'code';
|
|
48
|
+
|
|
49
|
+
const authUrl = 'https://authUrl';
|
|
50
|
+
const auth = { // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
51
|
+
generateAuthUrl: jest.fn().mockReturnValue(authUrl),
|
|
52
|
+
getToken : jest.fn().mockResolvedValue({ tokens: credentials }),
|
|
53
|
+
} as unknown as GoogleApis.Common.OAuth2Client;
|
|
54
|
+
|
|
55
|
+
server = new EventEmitter() as typeof server; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
56
|
+
|
|
57
|
+
jest.mocked(http.createServer).mockImplementation(() => server);
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
59
|
+
jest.mocked(open).mockImplementation(async (url: string) => makeRequest(url.replace('http://localhost:6006', '')));
|
|
60
|
+
|
|
61
|
+
jest.useFakeTimers();
|
|
62
|
+
|
|
63
|
+
jest.mocked(renderAuth).mockImplementation((
|
|
64
|
+
{ profile, authUrl, scope }: { profile: string; authUrl: string; scope: string[] },
|
|
65
|
+
) => `content = profile = ${profile} authUrl = ${authUrl} scope = ${scope.join('|')}`);
|
|
66
|
+
|
|
67
|
+
jest.mocked(renderDone).mockImplementation(() => 'content = done');
|
|
68
|
+
|
|
69
|
+
jest.mocked(getScopes).mockReturnValue([ 'scope1', 'scope2' ]);
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
mockFs({
|
|
73
|
+
[credentialsFile]: JSON.stringify(credentials),
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(() => {
|
|
78
|
+
mockFs.restore();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('src/lib/credentials/generator', () => {
|
|
82
|
+
describe('generateCredentials', () => {
|
|
83
|
+
const tokenUrl = `/request.url?code=${code}`;
|
|
84
|
+
|
|
85
|
+
const connections = [
|
|
86
|
+
{ remoteAddress: 'server', remotePort: '1001', on: jest.fn(), destroy: jest.fn() },
|
|
87
|
+
{ remoteAddress: 'server', remotePort: '1002', on: jest.fn(), destroy: jest.fn() },
|
|
88
|
+
{ remoteAddress: 'server', remotePort: '1003', on: jest.fn(), destroy: jest.fn() },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
let endSpy: jest.SpyInstance;
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
server = new EventEmitter() as typeof server; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
95
|
+
server.listen = jest.fn().mockImplementation(() => {
|
|
96
|
+
// always simulate opening several connections once connections are meant to be listened
|
|
97
|
+
connections.forEach((connection) => server.emit('connection', connection));
|
|
98
|
+
});
|
|
99
|
+
server.close = jest.fn();
|
|
100
|
+
server.destroy = jest.fn();
|
|
101
|
+
|
|
102
|
+
response = new EventEmitter() as typeof response; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
103
|
+
response.end = jest.fn();
|
|
104
|
+
|
|
105
|
+
endSpy = jest.spyOn(response, 'end');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterAll(() => {
|
|
109
|
+
endSpy.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should generate authUrl', async () => {
|
|
113
|
+
void generateCredentials(profile, auth);
|
|
114
|
+
await Promise.resolve();
|
|
115
|
+
|
|
116
|
+
expect(auth.generateAuthUrl).toHaveBeenCalledWith({ // eslint-disable-line @typescript-eslint/unbound-method
|
|
117
|
+
access_type: 'offline',
|
|
118
|
+
prompt : undefined,
|
|
119
|
+
scope : [ 'scope1', 'scope2' ],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should generate authUrl and require consent if explicitly asked', async () => {
|
|
124
|
+
void generateCredentials(profile, auth, { temporary: true }, 'consent');
|
|
125
|
+
await Promise.resolve();
|
|
126
|
+
|
|
127
|
+
expect(auth.generateAuthUrl).toHaveBeenCalledWith({ // eslint-disable-line @typescript-eslint/unbound-method
|
|
128
|
+
access_type: 'offline',
|
|
129
|
+
prompt : 'consent',
|
|
130
|
+
scope : [ 'scope1', 'scope2' ],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should generate authUrl with custom scopes', async () => {
|
|
135
|
+
void generateCredentials(profile, auth, { scopes: [ 'scope1', 'scope2' ] });
|
|
136
|
+
await Promise.resolve();
|
|
137
|
+
|
|
138
|
+
expect(auth.generateAuthUrl).toHaveBeenCalledWith({ // eslint-disable-line @typescript-eslint/unbound-method
|
|
139
|
+
access_type: 'offline',
|
|
140
|
+
prompt : undefined,
|
|
141
|
+
scope : [ 'scope1', 'scope2' ],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should create server on 6006 port', async () => {
|
|
146
|
+
void generateCredentials(profile, auth);
|
|
147
|
+
await Promise.resolve();
|
|
148
|
+
|
|
149
|
+
expect(http.createServer).toHaveBeenCalled();
|
|
150
|
+
expect(server.listen).toHaveBeenCalledWith(6006); // eslint-disable-line @typescript-eslint/unbound-method
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should open browser page and warn about it once listening', async () => {
|
|
154
|
+
void generateCredentials(profile, auth);
|
|
155
|
+
await Promise.resolve();
|
|
156
|
+
|
|
157
|
+
server.emit('listening');
|
|
158
|
+
|
|
159
|
+
expect(open).toHaveBeenCalledWith('http://localhost:6006/');
|
|
160
|
+
expect(logger.warn).toHaveBeenCalledWith('Please check your browser for further actions');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should not open browser page and warn about it until listening', async () => {
|
|
164
|
+
void generateCredentials(profile, auth);
|
|
165
|
+
await Promise.resolve();
|
|
166
|
+
|
|
167
|
+
expect(open).not.toHaveBeenCalled();
|
|
168
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should show nothing on the browser page if request.url is empty', async () => {
|
|
172
|
+
void generateCredentials(profile, auth);
|
|
173
|
+
makeRequest('');
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
|
|
176
|
+
expect(endSpy).toHaveBeenCalledWith('');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should show opening instructions if opened the home page', async () => {
|
|
180
|
+
void generateCredentials(profile, auth);
|
|
181
|
+
makeRequest('/');
|
|
182
|
+
await Promise.resolve();
|
|
183
|
+
|
|
184
|
+
expect(endSpy).toHaveBeenCalledWith('content = profile = username1 authUrl = https://authUrl scope = scope1|scope2');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should ask to close webpage', async () => {
|
|
188
|
+
void generateCredentials(profile, auth);
|
|
189
|
+
makeRequest(tokenUrl);
|
|
190
|
+
await Promise.resolve();
|
|
191
|
+
|
|
192
|
+
expect(endSpy).toHaveBeenCalledWith('content = done');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should close server and destroy all connections if request.url is truthy', async () => {
|
|
196
|
+
void generateCredentials(profile, auth);
|
|
197
|
+
makeRequest(tokenUrl);
|
|
198
|
+
await Promise.resolve();
|
|
199
|
+
|
|
200
|
+
expect(server.close).toHaveBeenCalled(); // eslint-disable-line @typescript-eslint/unbound-method
|
|
201
|
+
|
|
202
|
+
connections.forEach((connection) => {
|
|
203
|
+
expect(connection.destroy).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should close server and resolve if request.url is truthy', async () => {
|
|
208
|
+
const promise = generateCredentials(profile, auth);
|
|
209
|
+
makeRequest(tokenUrl);
|
|
210
|
+
const result = await Promise.resolve(promise);
|
|
211
|
+
expect(result).toEqual(credentials);
|
|
212
|
+
expect(server.close).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should not close server if request.url is falsy', async () => {
|
|
216
|
+
void generateCredentials(profile, auth);
|
|
217
|
+
makeRequest(undefined);
|
|
218
|
+
await Promise.resolve();
|
|
219
|
+
|
|
220
|
+
expect(server.close).not.toHaveBeenCalled(); // eslint-disable-line @typescript-eslint/unbound-method
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should re-throw a server error if error is not EADDRINUSE', () => {
|
|
224
|
+
const error = { code: 'RANDOM', message: 'random error' } as NodeJS.ErrnoException; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
225
|
+
|
|
226
|
+
void generateCredentials(profile, auth);
|
|
227
|
+
expect(() => server.emit('error', error)).toThrow(error.message);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should not re-throw a server error and try to listen again in 1000 seconds if error is EADDRINUSE', () => {
|
|
231
|
+
const error = { code: 'EADDRINUSE' } as NodeJS.ErrnoException; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
232
|
+
|
|
233
|
+
void generateCredentials(profile, auth);
|
|
234
|
+
expect(server.listen).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method
|
|
235
|
+
expect(() => server.emit('error', error)).not.toThrow();
|
|
236
|
+
expect(server.listen).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method
|
|
237
|
+
jest.advanceTimersByTime(1000);
|
|
238
|
+
expect(server.listen).toHaveBeenCalledTimes(2); // eslint-disable-line @typescript-eslint/unbound-method
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should return credentials JSON', async () => {
|
|
242
|
+
const promise = generateCredentials(profile, auth);
|
|
243
|
+
makeRequest(tokenUrl);
|
|
244
|
+
const result = await promise;
|
|
245
|
+
|
|
246
|
+
expect(result).toEqual(credentials);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import '@anmiles/prototypes';
|
|
4
|
+
import type GoogleApis from 'googleapis';
|
|
5
|
+
import mockFs from 'mock-fs';
|
|
6
|
+
|
|
7
|
+
import { getCredentialsFile } from '../../utils/paths';
|
|
8
|
+
import { generateCredentials } from '../generator';
|
|
9
|
+
import { deleteCredentials, getCredentials } from '../index';
|
|
10
|
+
import { validateCredentials } from '../validator';
|
|
11
|
+
|
|
12
|
+
jest.mock('../generator');
|
|
13
|
+
jest.mock('../validator');
|
|
14
|
+
|
|
15
|
+
const profile = 'username1';
|
|
16
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
17
|
+
|
|
18
|
+
const credentials: GoogleApis.Auth.Credentials = {
|
|
19
|
+
access_token: 'access_token_123',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const generatedCredentials: GoogleApis.Auth.Credentials = {
|
|
23
|
+
access_token : 'access_token_new',
|
|
24
|
+
refresh_token: 'refresh_token_new',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const scopes = [ 'scope1', 'scope2' ];
|
|
28
|
+
|
|
29
|
+
const authUrl = 'https://authUrl';
|
|
30
|
+
const auth = { // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
31
|
+
generateAuthUrl: jest.fn().mockReturnValue(authUrl),
|
|
32
|
+
getToken : jest.fn().mockResolvedValue({ tokens: credentials }),
|
|
33
|
+
} as unknown as GoogleApis.Common.OAuth2Client;
|
|
34
|
+
|
|
35
|
+
const generateCredentialsMock = jest.mocked(generateCredentials);
|
|
36
|
+
const validateCredentialsMock = jest.mocked(validateCredentials);
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
mockFs({
|
|
40
|
+
[credentialsFile]: JSON.stringify(credentials),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
generateCredentialsMock.mockResolvedValue(generatedCredentials);
|
|
44
|
+
validateCredentialsMock.mockReturnValue({ isValid: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
mockFs.restore();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('src/lib/credentials/index', () => {
|
|
52
|
+
describe('getCredentials', () => {
|
|
53
|
+
describe('on existing file', () => {
|
|
54
|
+
it('should return saved credentials', async () => {
|
|
55
|
+
const result = await getCredentials(profile, auth);
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual(credentials);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should not generate credentials', async () => {
|
|
61
|
+
await getCredentials(profile, auth);
|
|
62
|
+
|
|
63
|
+
expect(generateCredentialsMock).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should call validation on saved credentials', async () => {
|
|
67
|
+
await getCredentials(profile, auth);
|
|
68
|
+
|
|
69
|
+
expect(validateCredentialsMock).toHaveBeenCalledWith(credentials);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('on missing file', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
mockFs({});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return generated credentials', async () => {
|
|
79
|
+
const result = await getCredentials(profile, auth);
|
|
80
|
+
|
|
81
|
+
expect(result).toEqual(generatedCredentials);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should generate credentials with consent', async () => {
|
|
85
|
+
await getCredentials(profile, auth);
|
|
86
|
+
|
|
87
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, undefined, 'consent');
|
|
88
|
+
});
|
|
89
|
+
it('should generate credentials with consent and scopes', async () => {
|
|
90
|
+
await getCredentials(profile, auth, { scopes });
|
|
91
|
+
|
|
92
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, { scopes }, 'consent');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should call validation on generated credentials', async () => {
|
|
96
|
+
await getCredentials(profile, auth);
|
|
97
|
+
|
|
98
|
+
expect(validateCredentialsMock).toHaveBeenCalledWith(generatedCredentials);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw if saved credentials do not pass validation', async () => {
|
|
102
|
+
validateCredentialsMock.mockReturnValueOnce({ isValid: false, validationError: 'Test error' });
|
|
103
|
+
|
|
104
|
+
const promise = getCredentials(profile, auth);
|
|
105
|
+
|
|
106
|
+
await expect(promise).rejects.toEqual(new Error(`JSON created for ${credentialsFile} is not valid: Test error`));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('on existing file with failed validation', () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
validateCredentialsMock.mockReturnValueOnce({ isValid: false });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return generated credentials', async () => {
|
|
116
|
+
const result = await getCredentials(profile, auth);
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual(generatedCredentials);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should generate credentials with consent', async () => {
|
|
122
|
+
await getCredentials(profile, auth);
|
|
123
|
+
|
|
124
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, undefined, 'consent');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should generate credentials with consent and scopes', async () => {
|
|
128
|
+
await getCredentials(profile, auth, { scopes });
|
|
129
|
+
|
|
130
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, { scopes }, 'consent');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should generate credentials with no consent if existing credentials have refresh token', async () => {
|
|
134
|
+
mockFs({
|
|
135
|
+
[credentialsFile]: JSON.stringify({
|
|
136
|
+
...credentials,
|
|
137
|
+
refresh_token: 'refresh_token_123',
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await getCredentials(profile, auth);
|
|
142
|
+
|
|
143
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, undefined, undefined);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should call validation on generated credentials', async () => {
|
|
147
|
+
await getCredentials(profile, auth);
|
|
148
|
+
|
|
149
|
+
expect(validateCredentialsMock).toHaveBeenCalledWith(generatedCredentials);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should throw if generated credentials do not pass validation', async () => {
|
|
153
|
+
validateCredentialsMock.mockReturnValueOnce({ isValid: false, validationError: 'Test error' });
|
|
154
|
+
|
|
155
|
+
const promise = getCredentials(profile, auth);
|
|
156
|
+
|
|
157
|
+
await expect(promise).rejects.toEqual(new Error(`JSON created for ${credentialsFile} is not valid: Test error`));
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('temporary', () => {
|
|
162
|
+
it('should return generated credentials', async () => {
|
|
163
|
+
const result = await getCredentials(profile, auth, { temporary: true });
|
|
164
|
+
|
|
165
|
+
expect(result).toEqual(generatedCredentials);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should generate credentials with no consent', async () => {
|
|
169
|
+
await getCredentials(profile, auth, { temporary: true });
|
|
170
|
+
|
|
171
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, { temporary: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should generate credentials with scopes', async () => {
|
|
175
|
+
await getCredentials(profile, auth, { scopes, temporary: true });
|
|
176
|
+
|
|
177
|
+
expect(generateCredentialsMock).toHaveBeenCalledWith(profile, auth, { scopes, temporary: true });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should call validation on generated credentials', async () => {
|
|
181
|
+
await getCredentials(profile, auth, { temporary: true });
|
|
182
|
+
|
|
183
|
+
expect(validateCredentialsMock).toHaveBeenCalledWith(generatedCredentials, { temporary: true });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should throw if generated credentials do not pass validation', async () => {
|
|
187
|
+
validateCredentialsMock.mockReturnValueOnce({ isValid: false, validationError: 'Test error' });
|
|
188
|
+
|
|
189
|
+
const promise = getCredentials(profile, auth, { temporary: true });
|
|
190
|
+
|
|
191
|
+
await expect(promise).rejects.toEqual(new Error('Test error'));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('deleteCredentials', () => {
|
|
197
|
+
it('should delete credentials file if exists', () => {
|
|
198
|
+
expect(fs.existsSync(credentialsFile)).toEqual(true);
|
|
199
|
+
|
|
200
|
+
deleteCredentials(profile);
|
|
201
|
+
|
|
202
|
+
expect(fs.existsSync(credentialsFile)).toEqual(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should do nothing if credentials file does not exist', () => {
|
|
206
|
+
mockFs({});
|
|
207
|
+
|
|
208
|
+
deleteCredentials(profile);
|
|
209
|
+
|
|
210
|
+
expect(fs.existsSync(credentialsFile)).toEqual(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { validateCredentials } from '../validator';
|
|
2
|
+
|
|
3
|
+
describe('src/lib/credentials/validator', () => {
|
|
4
|
+
describe('validateCredentials', () => {
|
|
5
|
+
let expiryDate: Date;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
expiryDate = new Date();
|
|
9
|
+
expiryDate.setDate(expiryDate.getDate() - 6);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return false if no access token', () => {
|
|
13
|
+
expect(validateCredentials({ refresh_token: 'token', expiry_date: expiryDate.getTime() }))
|
|
14
|
+
.toEqual({ isValid: false, validationError: 'Credentials does not have access_token' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return false if no refresh token', () => {
|
|
18
|
+
expect(validateCredentials({ access_token: 'token', expiry_date: expiryDate.getTime() }))
|
|
19
|
+
.toEqual({ isValid: false, validationError: 'Credentials does not have refresh_token' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return true if no refresh token for temporary credentials', () => {
|
|
23
|
+
expect(validateCredentials({ access_token: 'token', expiry_date: expiryDate.getTime() }, { temporary: true }))
|
|
24
|
+
.toEqual({ isValid: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false if no expiration date', () => {
|
|
28
|
+
expect(validateCredentials({ access_token: 'token', refresh_token: 'token' }))
|
|
29
|
+
.toEqual({ isValid: false, validationError: 'Credentials does not have expiry_date' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return true if credentials are not more than 1 week ago', () => {
|
|
33
|
+
expect(validateCredentials({ access_token: 'token', refresh_token: 'token', expiry_date: expiryDate.getTime() }))
|
|
34
|
+
.toEqual({ isValid: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return false if credentials are more than 1 week ago', () => {
|
|
38
|
+
expiryDate.setDate(expiryDate.getDate() - 2);
|
|
39
|
+
expect(validateCredentials({ access_token: 'token', refresh_token: 'token', expiry_date: expiryDate.getTime() }))
|
|
40
|
+
.toEqual({ isValid: false, validationError: 'Credentials expired' });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|