@thoughtspot/visual-embed-sdk 1.10.0 → 1.10.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/dist/src/auth.d.ts +18 -5
- package/dist/src/embed/base.d.ts +21 -5
- package/dist/src/embed/pinboard.d.ts +91 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/types.d.ts +15 -0
- package/dist/src/utils/authService.d.ts +1 -0
- package/dist/src/utils/plugin.d.ts +0 -0
- package/dist/src/utils/processData.d.ts +1 -1
- package/dist/src/v1/api.d.ts +19 -0
- package/dist/tsembed.es.js +521 -32
- package/dist/tsembed.js +519 -31
- package/lib/package.json +2 -1
- package/lib/src/auth.d.ts +18 -5
- package/lib/src/auth.js +48 -9
- package/lib/src/auth.js.map +1 -1
- package/lib/src/auth.spec.js +69 -11
- package/lib/src/auth.spec.js.map +1 -1
- package/lib/src/embed/base.d.ts +21 -5
- package/lib/src/embed/base.js +64 -10
- package/lib/src/embed/base.js.map +1 -1
- package/lib/src/embed/base.spec.js +49 -3
- package/lib/src/embed/base.spec.js.map +1 -1
- package/lib/src/embed/embed.spec.js +1 -1
- package/lib/src/embed/embed.spec.js.map +1 -1
- package/lib/src/embed/pinboard.d.ts +91 -0
- package/lib/src/embed/pinboard.js +110 -0
- package/lib/src/embed/pinboard.js.map +1 -0
- package/lib/src/embed/ts-embed.js +9 -10
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +16 -6
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +3 -2
- package/lib/src/index.js +3 -2
- package/lib/src/index.js.map +1 -1
- package/lib/src/test/test-utils.js +1 -1
- package/lib/src/test/test-utils.js.map +1 -1
- package/lib/src/types.d.ts +15 -0
- package/lib/src/types.js +10 -0
- package/lib/src/types.js.map +1 -1
- package/lib/src/utils/authService.d.ts +1 -0
- package/lib/src/utils/authService.js +21 -3
- package/lib/src/utils/authService.js.map +1 -1
- package/lib/src/utils/authService.spec.js +21 -5
- package/lib/src/utils/authService.spec.js.map +1 -1
- package/lib/src/utils/plugin.d.ts +0 -0
- package/lib/src/utils/plugin.js +1 -0
- package/lib/src/utils/plugin.js.map +1 -0
- package/lib/src/utils/processData.d.ts +1 -1
- package/lib/src/utils/processData.js +37 -3
- package/lib/src/utils/processData.js.map +1 -1
- package/lib/src/utils/processData.spec.js +106 -4
- package/lib/src/utils/processData.spec.js.map +1 -1
- package/lib/src/visual-embed-sdk.d.ts +100 -7
- package/package.json +2 -1
- package/src/auth.spec.ts +90 -11
- package/src/auth.ts +63 -13
- package/src/embed/base.spec.ts +56 -4
- package/src/embed/base.ts +83 -16
- package/src/embed/embed.spec.ts +1 -1
- package/src/embed/ts-embed.spec.ts +19 -9
- package/src/embed/ts-embed.ts +15 -12
- package/src/index.ts +5 -1
- package/src/test/test-utils.ts +1 -1
- package/src/types.ts +16 -0
- package/src/utils/authService.spec.ts +31 -5
- package/src/utils/authService.ts +27 -3
- package/src/utils/processData.spec.ts +139 -4
- package/src/utils/processData.ts +54 -4
package/src/auth.spec.ts
CHANGED
|
@@ -9,12 +9,12 @@ const password = '12345678';
|
|
|
9
9
|
const samalLoginUrl = `${thoughtSpotHost}/callosum/v1/saml/login?targetURLPath=%235e16222e-ef02-43e9-9fbd-24226bf3ce5b`;
|
|
10
10
|
|
|
11
11
|
const embedConfig: any = {
|
|
12
|
-
doTokenAuthSuccess: {
|
|
12
|
+
doTokenAuthSuccess: (token: string) => ({
|
|
13
13
|
thoughtSpotHost,
|
|
14
14
|
username,
|
|
15
15
|
authEndpoint: 'auth',
|
|
16
|
-
getAuthToken: jest.fn(() => Promise.resolve(
|
|
17
|
-
},
|
|
16
|
+
getAuthToken: jest.fn(() => Promise.resolve(token)),
|
|
17
|
+
}),
|
|
18
18
|
doTokenAuthFailureWithoutAuthEndPoint: {
|
|
19
19
|
thoughtSpotHost,
|
|
20
20
|
username,
|
|
@@ -35,9 +35,15 @@ const embedConfig: any = {
|
|
|
35
35
|
doSamlAuth: {
|
|
36
36
|
thoughtSpotHost,
|
|
37
37
|
},
|
|
38
|
+
doOidcAuth: {
|
|
39
|
+
thoughtSpotHost,
|
|
40
|
+
},
|
|
38
41
|
SSOAuth: {
|
|
39
42
|
authType: AuthType.SSO,
|
|
40
43
|
},
|
|
44
|
+
OIDCAuth: {
|
|
45
|
+
authType: AuthType.OIDC,
|
|
46
|
+
},
|
|
41
47
|
authServerFailure: {
|
|
42
48
|
thoughtSpotHost,
|
|
43
49
|
username,
|
|
@@ -107,12 +113,14 @@ describe('Unit test for auth', () => {
|
|
|
107
113
|
status: 200,
|
|
108
114
|
}),
|
|
109
115
|
);
|
|
110
|
-
await authInstance.doTokenAuth(
|
|
116
|
+
await authInstance.doTokenAuth(
|
|
117
|
+
embedConfig.doTokenAuthSuccess('authToken'),
|
|
118
|
+
);
|
|
111
119
|
expect(authService.fetchSessionInfoService).toBeCalled();
|
|
112
120
|
expect(authInstance.loggedInStatus).toBe(true);
|
|
113
121
|
});
|
|
114
122
|
|
|
115
|
-
test('doTokenAuth: when user is not loggedIn & getAuthToken have response
|
|
123
|
+
test('doTokenAuth: when user is not loggedIn & getAuthToken have response', async () => {
|
|
116
124
|
jest.spyOn(authService, 'fetchSessionInfoService').mockImplementation(
|
|
117
125
|
() => false,
|
|
118
126
|
);
|
|
@@ -120,13 +128,19 @@ describe('Unit test for auth', () => {
|
|
|
120
128
|
authService,
|
|
121
129
|
'fetchAuthTokenService',
|
|
122
130
|
).mockImplementation(() => ({ text: () => Promise.resolve('abc') }));
|
|
123
|
-
jest.spyOn(authService, 'fetchAuthService')
|
|
124
|
-
|
|
131
|
+
jest.spyOn(authService, 'fetchAuthService').mockImplementation(() =>
|
|
132
|
+
Promise.resolve({
|
|
133
|
+
status: 200,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
await authInstance.doTokenAuth(
|
|
137
|
+
embedConfig.doTokenAuthSuccess('authToken2'),
|
|
138
|
+
);
|
|
125
139
|
expect(authService.fetchSessionInfoService).toBeCalled();
|
|
126
140
|
expect(authService.fetchAuthService).toBeCalledWith(
|
|
127
141
|
thoughtSpotHost,
|
|
128
142
|
username,
|
|
129
|
-
'
|
|
143
|
+
'authToken2',
|
|
130
144
|
);
|
|
131
145
|
});
|
|
132
146
|
|
|
@@ -140,7 +154,12 @@ describe('Unit test for auth', () => {
|
|
|
140
154
|
).mockImplementation(() =>
|
|
141
155
|
Promise.resolve({ text: () => Promise.resolve('abc') }),
|
|
142
156
|
);
|
|
143
|
-
jest.spyOn(authService, 'fetchAuthService')
|
|
157
|
+
jest.spyOn(authService, 'fetchAuthService').mockImplementation(() =>
|
|
158
|
+
Promise.resolve({
|
|
159
|
+
status: 200,
|
|
160
|
+
ok: true,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
144
163
|
await authInstance.doTokenAuth(
|
|
145
164
|
embedConfig.doTokenAuthFailureWithoutGetAuthToken,
|
|
146
165
|
);
|
|
@@ -155,6 +174,38 @@ describe('Unit test for auth', () => {
|
|
|
155
174
|
});
|
|
156
175
|
});
|
|
157
176
|
|
|
177
|
+
test('doTokenAuth: Should raise error when duplicate token is used', async () => {
|
|
178
|
+
jest.spyOn(authService, 'fetchSessionInfoService').mockResolvedValue({
|
|
179
|
+
status: 401,
|
|
180
|
+
});
|
|
181
|
+
jest.spyOn(window, 'alert').mockClear();
|
|
182
|
+
jest.spyOn(window, 'alert').mockReturnValue(undefined);
|
|
183
|
+
jest.spyOn(authService, 'fetchAuthService').mockReset();
|
|
184
|
+
jest.spyOn(authService, 'fetchAuthService').mockImplementation(() =>
|
|
185
|
+
Promise.resolve({
|
|
186
|
+
status: 200,
|
|
187
|
+
ok: true,
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
await authInstance.doTokenAuth(
|
|
191
|
+
embedConfig.doTokenAuthSuccess('authToken3'),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await authInstance.doTokenAuth(
|
|
196
|
+
embedConfig.doTokenAuthSuccess('authToken3'),
|
|
197
|
+
);
|
|
198
|
+
expect(false).toBe(true);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
expect(e.message).toContain('Duplicate token');
|
|
201
|
+
}
|
|
202
|
+
await executeAfterWait(() => {
|
|
203
|
+
expect(authInstance.loggedInStatus).toBe(false);
|
|
204
|
+
expect(window.alert).toBeCalled();
|
|
205
|
+
expect(authService.fetchAuthService).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
158
209
|
describe('doBasicAuth', () => {
|
|
159
210
|
beforeEach(() => {
|
|
160
211
|
global.fetch = window.fetch;
|
|
@@ -181,7 +232,7 @@ describe('Unit test for auth', () => {
|
|
|
181
232
|
jest.spyOn(
|
|
182
233
|
authService,
|
|
183
234
|
'fetchBasicAuthService',
|
|
184
|
-
).mockImplementation(() => ({ status: 200 }));
|
|
235
|
+
).mockImplementation(() => ({ status: 200, ok: true }));
|
|
185
236
|
|
|
186
237
|
await authInstance.doBasicAuth(embedConfig.doBasicAuth);
|
|
187
238
|
expect(authService.fetchSessionInfoService).toBeCalled();
|
|
@@ -253,6 +304,7 @@ describe('Unit test for auth', () => {
|
|
|
253
304
|
},
|
|
254
305
|
});
|
|
255
306
|
spyOn(authInstance, 'samlCompletionPromise');
|
|
307
|
+
global.window.open = jest.fn();
|
|
256
308
|
jest.spyOn(
|
|
257
309
|
authService,
|
|
258
310
|
'fetchSessionInfoService',
|
|
@@ -263,8 +315,28 @@ describe('Unit test for auth', () => {
|
|
|
263
315
|
...embedConfig.doSamlAuth,
|
|
264
316
|
noRedirect: true,
|
|
265
317
|
}),
|
|
266
|
-
).toBe(
|
|
318
|
+
).toBe(true);
|
|
319
|
+
expect(authService.fetchSessionInfoService).toBeCalled();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('doOIDCAuth', () => {
|
|
324
|
+
afterEach(() => {
|
|
325
|
+
delete global.window;
|
|
326
|
+
global.window = Object.create(originalWindow);
|
|
327
|
+
global.window.open = jest.fn();
|
|
328
|
+
global.fetch = window.fetch;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('when user is not loggedIn & isAtSSORedirectUrl is true', async () => {
|
|
332
|
+
jest.spyOn(
|
|
333
|
+
authService,
|
|
334
|
+
'fetchSessionInfoService',
|
|
335
|
+
).mockImplementation(() => Promise.reject());
|
|
336
|
+
await authInstance.doOIDCAuth(embedConfig.doOidcAuth);
|
|
267
337
|
expect(authService.fetchSessionInfoService).toBeCalled();
|
|
338
|
+
expect(window.location.hash).toBe('');
|
|
339
|
+
expect(authInstance.loggedInStatus).toBe(false);
|
|
268
340
|
});
|
|
269
341
|
});
|
|
270
342
|
|
|
@@ -275,6 +347,13 @@ describe('Unit test for auth', () => {
|
|
|
275
347
|
expect(authInstance.doSamlAuth).toBeCalled();
|
|
276
348
|
});
|
|
277
349
|
|
|
350
|
+
it('authenticate: when authType is OIDC', async () => {
|
|
351
|
+
jest.spyOn(authInstance, 'doOIDCAuth');
|
|
352
|
+
await authInstance.authenticate(embedConfig.OIDCAuth);
|
|
353
|
+
expect(window.location.hash).toBe('');
|
|
354
|
+
expect(authInstance.doOIDCAuth).toBeCalled();
|
|
355
|
+
});
|
|
356
|
+
|
|
278
357
|
it('authenticate: when authType is AuthServer', async () => {
|
|
279
358
|
spyOn(authInstance, 'doTokenAuth');
|
|
280
359
|
await authInstance.authenticate(embedConfig.authServerFailure);
|
package/src/auth.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
fetchAuthTokenService,
|
|
8
8
|
fetchAuthService,
|
|
9
9
|
fetchBasicAuthService,
|
|
10
|
+
fetchLogoutService,
|
|
10
11
|
} from './utils/authService';
|
|
11
12
|
|
|
12
13
|
// eslint-disable-next-line import/no-mutable-exports
|
|
@@ -29,8 +30,22 @@ export const EndPoints = {
|
|
|
29
30
|
`/callosum/v1/oidc/login?targetURLPath=${targetUrl}`,
|
|
30
31
|
TOKEN_LOGIN: '/callosum/v1/session/login/token',
|
|
31
32
|
BASIC_LOGIN: '/callosum/v1/session/login',
|
|
33
|
+
LOGOUT: '/callosum/v1/session/logout',
|
|
32
34
|
};
|
|
33
35
|
|
|
36
|
+
export enum AuthFailureType {
|
|
37
|
+
SDK = 'SDK',
|
|
38
|
+
NO_COOKIE_ACCESS = 'NO_COOKIE_ACCESS',
|
|
39
|
+
EXPIRY = 'EXPIRY',
|
|
40
|
+
OTHER = 'OTHER',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum AuthStatus {
|
|
44
|
+
FAILURE = 'FAILURE',
|
|
45
|
+
SUCCESS = 'SUCCESS',
|
|
46
|
+
LOGOUT = 'LOGOUT',
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Check if we are logged into the ThoughtSpot cluster
|
|
36
51
|
* @param thoughtSpotHost The ThoughtSpot cluster hostname or IP
|
|
@@ -58,6 +73,19 @@ export function initSession(sessionDetails: any) {
|
|
|
58
73
|
initMixpanel(sessionInfo);
|
|
59
74
|
}
|
|
60
75
|
|
|
76
|
+
const DUPLICATE_TOKEN_ERR =
|
|
77
|
+
'Duplicate token, please issue a new token every time getAuthToken callback is called.' +
|
|
78
|
+
'See https://developers.thoughtspot.com/docs/?pageid=embed-auth#trusted-auth-embed for more details.';
|
|
79
|
+
let prevAuthToken: string = null;
|
|
80
|
+
function alertForDuplicateToken(authtoken: string) {
|
|
81
|
+
if (prevAuthToken === authtoken) {
|
|
82
|
+
// eslint-disable-next-line no-alert
|
|
83
|
+
alert(DUPLICATE_TOKEN_ERR);
|
|
84
|
+
throw new Error(DUPLICATE_TOKEN_ERR);
|
|
85
|
+
}
|
|
86
|
+
prevAuthToken = authtoken;
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/**
|
|
62
90
|
* Check if we are stuck at the SSO redirect URL
|
|
63
91
|
*/
|
|
@@ -83,7 +111,9 @@ function removeSSORedirectUrlMarker(): void {
|
|
|
83
111
|
* Perform token based authentication
|
|
84
112
|
* @param embedConfig The embed configuration
|
|
85
113
|
*/
|
|
86
|
-
export const doTokenAuth = async (
|
|
114
|
+
export const doTokenAuth = async (
|
|
115
|
+
embedConfig: EmbedConfig,
|
|
116
|
+
): Promise<boolean> => {
|
|
87
117
|
const {
|
|
88
118
|
thoughtSpotHost,
|
|
89
119
|
username,
|
|
@@ -95,20 +125,25 @@ export const doTokenAuth = async (embedConfig: EmbedConfig): Promise<void> => {
|
|
|
95
125
|
'Either auth endpoint or getAuthToken function must be provided',
|
|
96
126
|
);
|
|
97
127
|
}
|
|
98
|
-
|
|
99
|
-
if (!
|
|
128
|
+
loggedInStatus = await isLoggedIn(thoughtSpotHost);
|
|
129
|
+
if (!loggedInStatus) {
|
|
100
130
|
let authToken = null;
|
|
101
131
|
if (getAuthToken) {
|
|
102
132
|
authToken = await getAuthToken();
|
|
133
|
+
alertForDuplicateToken(authToken);
|
|
103
134
|
} else {
|
|
104
135
|
const response = await fetchAuthTokenService(authEndpoint);
|
|
105
136
|
authToken = await response.text();
|
|
106
137
|
}
|
|
107
|
-
await fetchAuthService(
|
|
108
|
-
|
|
138
|
+
const resp = await fetchAuthService(
|
|
139
|
+
thoughtSpotHost,
|
|
140
|
+
username,
|
|
141
|
+
authToken,
|
|
142
|
+
);
|
|
143
|
+
// token login issues a 302 when successful
|
|
144
|
+
loggedInStatus = resp.ok || resp.type === 'opaqueredirect';
|
|
109
145
|
}
|
|
110
|
-
|
|
111
|
-
loggedInStatus = true;
|
|
146
|
+
return loggedInStatus;
|
|
112
147
|
};
|
|
113
148
|
|
|
114
149
|
/**
|
|
@@ -119,7 +154,9 @@ export const doTokenAuth = async (embedConfig: EmbedConfig): Promise<void> => {
|
|
|
119
154
|
* strongly advised not to use this authentication method in production.
|
|
120
155
|
* @param embedConfig The embed configuration
|
|
121
156
|
*/
|
|
122
|
-
export const doBasicAuth = async (
|
|
157
|
+
export const doBasicAuth = async (
|
|
158
|
+
embedConfig: EmbedConfig,
|
|
159
|
+
): Promise<boolean> => {
|
|
123
160
|
const { thoughtSpotHost, username, password } = embedConfig;
|
|
124
161
|
const loggedIn = await isLoggedIn(thoughtSpotHost);
|
|
125
162
|
if (!loggedIn) {
|
|
@@ -128,10 +165,11 @@ export const doBasicAuth = async (embedConfig: EmbedConfig): Promise<void> => {
|
|
|
128
165
|
username,
|
|
129
166
|
password,
|
|
130
167
|
);
|
|
131
|
-
loggedInStatus = response.
|
|
168
|
+
loggedInStatus = response.ok;
|
|
169
|
+
} else {
|
|
170
|
+
loggedInStatus = true;
|
|
132
171
|
}
|
|
133
|
-
|
|
134
|
-
loggedInStatus = true;
|
|
172
|
+
return loggedInStatus;
|
|
135
173
|
};
|
|
136
174
|
|
|
137
175
|
async function samlPopupFlow(ssoURL: string) {
|
|
@@ -198,6 +236,7 @@ const doSSOAuth = async (
|
|
|
198
236
|
const ssoURL = `${thoughtSpotHost}${ssoEndPoint}`;
|
|
199
237
|
if (embedConfig.noRedirect) {
|
|
200
238
|
await samlPopupFlow(ssoURL);
|
|
239
|
+
loggedInStatus = true;
|
|
201
240
|
return;
|
|
202
241
|
}
|
|
203
242
|
|
|
@@ -218,6 +257,7 @@ export const doSamlAuth = async (embedConfig: EmbedConfig) => {
|
|
|
218
257
|
)}`;
|
|
219
258
|
|
|
220
259
|
await doSSOAuth(embedConfig, ssoEndPoint);
|
|
260
|
+
return loggedInStatus;
|
|
221
261
|
};
|
|
222
262
|
|
|
223
263
|
export const doOIDCAuth = async (embedConfig: EmbedConfig) => {
|
|
@@ -234,13 +274,23 @@ export const doOIDCAuth = async (embedConfig: EmbedConfig) => {
|
|
|
234
274
|
)}`;
|
|
235
275
|
|
|
236
276
|
await doSSOAuth(embedConfig, ssoEndPoint);
|
|
277
|
+
return loggedInStatus;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const logout = async (embedConfig: EmbedConfig): Promise<boolean> => {
|
|
281
|
+
const { thoughtSpotHost } = embedConfig;
|
|
282
|
+
const response = await fetchLogoutService(thoughtSpotHost);
|
|
283
|
+
loggedInStatus = false;
|
|
284
|
+
return loggedInStatus;
|
|
237
285
|
};
|
|
238
286
|
|
|
239
287
|
/**
|
|
240
288
|
* Perform authentication on the ThoughtSpot cluster
|
|
241
289
|
* @param embedConfig The embed configuration
|
|
242
290
|
*/
|
|
243
|
-
export const authenticate = async (
|
|
291
|
+
export const authenticate = async (
|
|
292
|
+
embedConfig: EmbedConfig,
|
|
293
|
+
): Promise<boolean> => {
|
|
244
294
|
const { authType } = embedConfig;
|
|
245
295
|
switch (authType) {
|
|
246
296
|
case AuthType.SSO:
|
|
@@ -252,7 +302,7 @@ export const authenticate = async (embedConfig: EmbedConfig): Promise<void> => {
|
|
|
252
302
|
case AuthType.Basic:
|
|
253
303
|
return doBasicAuth(embedConfig);
|
|
254
304
|
default:
|
|
255
|
-
return Promise.resolve();
|
|
305
|
+
return Promise.resolve(true);
|
|
256
306
|
}
|
|
257
307
|
};
|
|
258
308
|
|
package/src/embed/base.spec.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
|
2
|
+
import * as auth from '../auth';
|
|
1
3
|
import * as index from '../index';
|
|
4
|
+
import * as base from './base';
|
|
2
5
|
import {
|
|
3
6
|
executeAfterWait,
|
|
4
7
|
getAllIframeEl,
|
|
@@ -9,10 +12,11 @@ import {
|
|
|
9
12
|
} from '../test/test-utils';
|
|
10
13
|
|
|
11
14
|
const thoughtSpotHost = 'tshost';
|
|
15
|
+
let authEE: EventEmitter;
|
|
12
16
|
|
|
13
17
|
describe('Base TS Embed', () => {
|
|
14
18
|
beforeAll(() => {
|
|
15
|
-
index.init({
|
|
19
|
+
authEE = index.init({
|
|
16
20
|
thoughtSpotHost,
|
|
17
21
|
authType: index.AuthType.None,
|
|
18
22
|
});
|
|
@@ -38,10 +42,12 @@ describe('Base TS Embed', () => {
|
|
|
38
42
|
},
|
|
39
43
|
'*',
|
|
40
44
|
);
|
|
41
|
-
|
|
42
|
-
jest.spyOn(window, 'alert').mockImplementation(() =>
|
|
45
|
+
jest.spyOn(window, 'alert').mockReset();
|
|
46
|
+
jest.spyOn(window, 'alert').mockImplementation(() => undefined);
|
|
47
|
+
authEE.on(auth.AuthStatus.FAILURE, (reason) => {
|
|
48
|
+
expect(reason).toEqual(auth.AuthFailureType.NO_COOKIE_ACCESS);
|
|
43
49
|
expect(window.alert).toBeCalledWith(
|
|
44
|
-
'Third party cookie access is blocked on this browser, please allow third party cookies for
|
|
50
|
+
'Third party cookie access is blocked on this browser, please allow third party cookies for this to work properly. \nYou can use `suppressNoCookieAccessAlert` to suppress this message.',
|
|
45
51
|
);
|
|
46
52
|
done();
|
|
47
53
|
});
|
|
@@ -92,4 +98,50 @@ describe('Base TS Embed', () => {
|
|
|
92
98
|
expect(getIFrameSrc()).toContain('disableLoginRedirect=true');
|
|
93
99
|
});
|
|
94
100
|
});
|
|
101
|
+
|
|
102
|
+
test('handleAuth notifies for SDK auth failure', (done) => {
|
|
103
|
+
jest.spyOn(auth, 'authenticate').mockResolvedValue(false);
|
|
104
|
+
const authEmitter = index.init({
|
|
105
|
+
thoughtSpotHost,
|
|
106
|
+
authType: index.AuthType.Basic,
|
|
107
|
+
username: 'test',
|
|
108
|
+
password: 'test',
|
|
109
|
+
});
|
|
110
|
+
authEmitter.on(auth.AuthStatus.FAILURE, (reason) => {
|
|
111
|
+
expect(reason).toBe(auth.AuthFailureType.SDK);
|
|
112
|
+
done();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Logout method should disable autoLogin', () => {
|
|
117
|
+
jest.spyOn(window, 'fetch').mockResolvedValue({
|
|
118
|
+
type: 'opaque',
|
|
119
|
+
});
|
|
120
|
+
index.init({
|
|
121
|
+
thoughtSpotHost,
|
|
122
|
+
authType: index.AuthType.None,
|
|
123
|
+
autoLogin: true,
|
|
124
|
+
});
|
|
125
|
+
index.logout();
|
|
126
|
+
expect(window.fetch).toHaveBeenCalledWith(
|
|
127
|
+
`http://${thoughtSpotHost}${auth.EndPoints.LOGOUT}`,
|
|
128
|
+
{
|
|
129
|
+
credentials: 'include',
|
|
130
|
+
mode: 'no-cors',
|
|
131
|
+
method: 'POST',
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
expect(base.getEmbedConfig().autoLogin).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('Base without init', () => {
|
|
139
|
+
test('notify should error when called without init', () => {
|
|
140
|
+
base.reset();
|
|
141
|
+
jest.spyOn(global.console, 'error').mockImplementation(() => undefined);
|
|
142
|
+
base.notifyAuthSuccess();
|
|
143
|
+
base.notifyAuthFailure(auth.AuthFailureType.SDK);
|
|
144
|
+
base.notifyLogout();
|
|
145
|
+
expect(global.console.error).toHaveBeenCalledTimes(3);
|
|
146
|
+
});
|
|
95
147
|
});
|
package/src/embed/base.ts
CHANGED
|
@@ -7,31 +7,72 @@
|
|
|
7
7
|
* @summary Base classes
|
|
8
8
|
* @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
|
|
9
9
|
*/
|
|
10
|
+
import EventEmitter from 'eventemitter3';
|
|
10
11
|
import { getThoughtSpotHost } from '../config';
|
|
11
|
-
import { EmbedConfig } from '../types';
|
|
12
|
-
import {
|
|
12
|
+
import { AuthType, EmbedConfig } from '../types';
|
|
13
|
+
import {
|
|
14
|
+
authenticate,
|
|
15
|
+
logout as _logout,
|
|
16
|
+
AuthFailureType,
|
|
17
|
+
AuthStatus,
|
|
18
|
+
} from '../auth';
|
|
13
19
|
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
|
|
14
20
|
|
|
15
21
|
let config = {} as EmbedConfig;
|
|
22
|
+
const CONFIG_DEFAULTS: Partial<EmbedConfig> = {
|
|
23
|
+
loginFailedMessage: 'Not logged in',
|
|
24
|
+
authType: AuthType.None,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export let authPromise: Promise<boolean>;
|
|
28
|
+
|
|
29
|
+
export const getEmbedConfig = (): EmbedConfig => config;
|
|
30
|
+
|
|
31
|
+
export const getAuthPromise = (): Promise<boolean> => authPromise;
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
let authEE: EventEmitter;
|
|
18
34
|
|
|
35
|
+
export function notifyAuthSuccess(): void {
|
|
36
|
+
if (!authEE) {
|
|
37
|
+
console.error('SDK not initialized');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
authEE.emit(AuthStatus.SUCCESS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function notifyAuthFailure(failureType: AuthFailureType): void {
|
|
44
|
+
if (!authEE) {
|
|
45
|
+
console.error('SDK not initialized');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
authEE.emit(AuthStatus.FAILURE, failureType);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function notifyLogout(): void {
|
|
52
|
+
if (!authEE) {
|
|
53
|
+
console.error('SDK not initialized');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
authEE.emit(AuthStatus.LOGOUT);
|
|
57
|
+
}
|
|
19
58
|
/**
|
|
20
59
|
* Perform authentication on the ThoughtSpot app as applicable.
|
|
21
60
|
*/
|
|
22
|
-
export const handleAuth = (): Promise<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
61
|
+
export const handleAuth = (): Promise<boolean> => {
|
|
62
|
+
authPromise = authenticate(config);
|
|
63
|
+
authPromise.then(
|
|
64
|
+
(isLoggedIn) => {
|
|
65
|
+
if (!isLoggedIn) {
|
|
66
|
+
notifyAuthFailure(AuthFailureType.SDK);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
() => {
|
|
70
|
+
notifyAuthFailure(AuthFailureType.SDK);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
28
73
|
return authPromise;
|
|
29
74
|
};
|
|
30
75
|
|
|
31
|
-
export const getEmbedConfig = (): EmbedConfig => config;
|
|
32
|
-
|
|
33
|
-
export const getAuthPromise = (): Promise<void> => authPromise;
|
|
34
|
-
|
|
35
76
|
/**
|
|
36
77
|
* Prefetches static resources from the specified URL. Web browsers can then cache the prefetched resources and serve them from the user's local disk to provide faster access to your app.
|
|
37
78
|
* @param url The URL provided for prefetch
|
|
@@ -59,8 +100,13 @@ export const prefetch = (url?: string): void => {
|
|
|
59
100
|
*
|
|
60
101
|
* @returns authPromise Promise which resolves when authentication is complete.
|
|
61
102
|
*/
|
|
62
|
-
export const init = (embedConfig: EmbedConfig):
|
|
63
|
-
config =
|
|
103
|
+
export const init = (embedConfig: EmbedConfig): EventEmitter => {
|
|
104
|
+
config = {
|
|
105
|
+
...CONFIG_DEFAULTS,
|
|
106
|
+
...embedConfig,
|
|
107
|
+
thoughtSpotHost: getThoughtSpotHost(embedConfig),
|
|
108
|
+
};
|
|
109
|
+
authEE = new EventEmitter();
|
|
64
110
|
handleAuth();
|
|
65
111
|
|
|
66
112
|
uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_CALLED_INIT, {
|
|
@@ -71,7 +117,21 @@ export const init = (embedConfig: EmbedConfig): Promise<void> => {
|
|
|
71
117
|
if (config.callPrefetch) {
|
|
72
118
|
prefetch(config.thoughtSpotHost);
|
|
73
119
|
}
|
|
74
|
-
return
|
|
120
|
+
return authEE;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function disableAutoLogin(): void {
|
|
124
|
+
config.autoLogin = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const logout = (doNotDisableAutoLogin = false): Promise<boolean> => {
|
|
128
|
+
if (!doNotDisableAutoLogin) {
|
|
129
|
+
disableAutoLogin();
|
|
130
|
+
}
|
|
131
|
+
return _logout(config).then((isLoggedIn) => {
|
|
132
|
+
notifyLogout();
|
|
133
|
+
return isLoggedIn;
|
|
134
|
+
});
|
|
75
135
|
};
|
|
76
136
|
|
|
77
137
|
let renderQueue: Promise<any> = Promise.resolve();
|
|
@@ -89,3 +149,10 @@ export const renderInQueue = (fn: (next?: (val?: any) => void) => void) => {
|
|
|
89
149
|
fn(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
|
|
90
150
|
}
|
|
91
151
|
};
|
|
152
|
+
|
|
153
|
+
// For testing purposes only
|
|
154
|
+
export function reset(): void {
|
|
155
|
+
config = {} as any;
|
|
156
|
+
authEE = null;
|
|
157
|
+
authPromise = null;
|
|
158
|
+
}
|
package/src/embed/embed.spec.ts
CHANGED
|
@@ -71,7 +71,7 @@ describe('Custom CSS Url', () => {
|
|
|
71
71
|
document.body.innerHTML = getDocumentBody();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
test
|
|
74
|
+
test('passing customCssUrl should set the correct query params on the iframe', async (done) => {
|
|
75
75
|
init({
|
|
76
76
|
thoughtSpotHost,
|
|
77
77
|
authType: AuthType.None,
|
|
@@ -165,15 +165,16 @@ describe('Unit test case for ts embed', () => {
|
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
-
describe('when thoughtSpotHost have value and authPromise return
|
|
168
|
+
describe('when thoughtSpotHost have value and authPromise return response true/false', () => {
|
|
169
169
|
beforeAll(() => {
|
|
170
170
|
init({
|
|
171
171
|
thoughtSpotHost,
|
|
172
172
|
authType: AuthType.None,
|
|
173
|
+
loginFailedMessage: 'Failed to Login',
|
|
173
174
|
});
|
|
174
175
|
});
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
const setup = async (isLoggedIn = false) => {
|
|
177
178
|
jest.spyOn(window, 'addEventListener').mockImplementationOnce(
|
|
178
179
|
(event, handler, options) => {
|
|
179
180
|
handler({
|
|
@@ -186,10 +187,9 @@ describe('Unit test case for ts embed', () => {
|
|
|
186
187
|
},
|
|
187
188
|
);
|
|
188
189
|
const iFrame: any = document.createElement('div');
|
|
189
|
-
jest.spyOn(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
).mockResolvedValueOnce(() => Promise.resolve());
|
|
190
|
+
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(
|
|
191
|
+
isLoggedIn,
|
|
192
|
+
);
|
|
193
193
|
const tsEmbed = new SearchEmbed(getRootEl(), {});
|
|
194
194
|
iFrame.contentWindow = null;
|
|
195
195
|
tsEmbed.on(EmbedEvent.CustomAction, jest.fn());
|
|
@@ -199,10 +199,11 @@ describe('Unit test case for ts embed', () => {
|
|
|
199
199
|
},
|
|
200
200
|
);
|
|
201
201
|
jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame);
|
|
202
|
-
tsEmbed.render();
|
|
203
|
-
}
|
|
202
|
+
await tsEmbed.render();
|
|
203
|
+
};
|
|
204
204
|
|
|
205
|
-
test('mixpanel should call with VISUAL_SDK_RENDER_COMPLETE', () => {
|
|
205
|
+
test('mixpanel should call with VISUAL_SDK_RENDER_COMPLETE', async () => {
|
|
206
|
+
await setup(true);
|
|
206
207
|
expect(mockMixPanelEvent).toBeCalledWith(
|
|
207
208
|
MIXPANEL_EVENT.VISUAL_SDK_RENDER_START,
|
|
208
209
|
);
|
|
@@ -212,11 +213,20 @@ describe('Unit test case for ts embed', () => {
|
|
|
212
213
|
});
|
|
213
214
|
|
|
214
215
|
test('Should remove prefetch iframe', async () => {
|
|
216
|
+
await setup(true);
|
|
215
217
|
const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>(
|
|
216
218
|
'.prefetchIframe',
|
|
217
219
|
);
|
|
218
220
|
expect(prefetchIframe.length).toBe(0);
|
|
219
221
|
});
|
|
222
|
+
|
|
223
|
+
test('Should render failure when login fails', async (done) => {
|
|
224
|
+
setup(false);
|
|
225
|
+
executeAfterWait(() => {
|
|
226
|
+
expect(getRootEl().innerHTML).toContain('Failed to Login');
|
|
227
|
+
done();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
220
230
|
});
|
|
221
231
|
|
|
222
232
|
describe('when thoughtSpotHost have value and authPromise return error', () => {
|