@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
510 lines (473 loc) • 18.8 kB
text/typescript
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-shadow */
import EventEmitter from 'eventemitter3';
import { EmbedConfig } from '../index';
import * as auth from '../auth';
import * as authService from '../utils/authService/authService';
import * as tokenAuthServices from '../utils/authService/tokenizedAuthService';
import * as authTokenService from '../authToken';
import * as index from '../index';
import * as base from './base';
import * as embedConfigInstance from './embedConfig';
import * as resetService from '../utils/resetServices';
import {
executeAfterWait,
getAllIframeEl,
getDocumentBody,
getRootEl,
getIFrameSrc,
} from '../test/test-utils';
import * as tokenizedFetchInstance from '../tokenizedFetch';
import { logger } from '../utils/logger';
const thoughtSpotHost = 'tshost';
let authEE: EventEmitter;
describe('Base TS Embed', () => {
beforeAll(() => {
authEE = index.init({
thoughtSpotHost,
authType: index.AuthType.None,
}) as EventEmitter;
jest.spyOn(auth, 'postLoginService').mockImplementation(() => Promise.resolve({}));
});
beforeEach(() => {
document.body.innerHTML = getDocumentBody();
});
test('Should show an alert when third party cookie access is blocked', (done) => {
const tsEmbed = new index.SearchEmbed(getRootEl(), {});
const iFrame: any = document.createElement('div');
iFrame.contentWindow = null;
/* This will return a div instead of HTMLIframeElement in ts-embed.ts
* so that the promise doesn't fail on url assigment
*/
jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame);
tsEmbed.render();
window.postMessage(
{
__type: index.EmbedEvent.NoCookieAccess,
},
'*',
);
jest.spyOn(window, 'alert').mockReset();
jest.spyOn(window, 'alert').mockImplementation(() => undefined);
authEE.on(auth.AuthStatus.FAILURE, (reason) => {
expect(reason).toEqual(auth.AuthFailureType.NO_COOKIE_ACCESS);
expect(window.alert).toBeCalledWith(
'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.',
);
done();
});
});
test('Should ignore cookie blocked alert if ignoreNoCookieAccess is true', async (done) => {
jest.spyOn(window, 'fetch').mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({}),
});
const authEE = index.init({
thoughtSpotHost,
authType: index.AuthType.None,
ignoreNoCookieAccess: true,
});
const tsEmbed = new index.SearchEmbed(getRootEl(), {});
const iFrame: any = document.createElement('div');
iFrame.contentWindow = null;
/* This will return a div instead of HTMLIframeElement in ts-embed.ts
* so that the promise doesn't fail on url assigment
*/
jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame);
tsEmbed.render();
window.postMessage(
{
__type: index.EmbedEvent.NoCookieAccess,
},
'*',
);
jest.spyOn(window, 'alert').mockReset();
jest.spyOn(window, 'alert').mockImplementation(() => undefined);
authEE.on(auth.AuthStatus.FAILURE, (reason) => {
expect(reason).toEqual(auth.AuthFailureType.NO_COOKIE_ACCESS);
expect(window.alert).not.toHaveBeenCalled();
done();
});
});
test('should call the executeTML API and import TML', async () => {
jest.spyOn(window, 'fetch').mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({}),
});
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
const data: base.executeTMLInput = {
metadata_tmls: ['{"liveboard":{"name":"Parameters Liveboard"}}'],
import_policy: 'PARTIAL',
create_new: false,
};
await index.executeTML(data);
expect(window.fetch).toHaveBeenCalledWith(
`http://${thoughtSpotHost}${authService.EndPoints.EXECUTE_TML}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'x-requested-by': 'ThoughtSpot',
},
body: JSON.stringify(data),
method: 'POST',
},
);
});
test('should call the executeTML API and import TML for cookiless auth', async () => {
jest.spyOn(authTokenService, 'getAuthenticationToken').mockResolvedValue('mockAuthToken');
jest.spyOn(tokenizedFetchInstance, 'tokenizedFetch').mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValue({}),
});
index.init({
thoughtSpotHost,
authType: index.AuthType.TrustedAuthTokenCookieless,
autoLogin: true,
});
const data: base.executeTMLInput = {
metadata_tmls: ['{"liveboard":{"name":"Parameters Liveboard"}}'],
import_policy: 'PARTIAL',
create_new: false,
};
await index.executeTML(data);
expect(tokenizedFetchInstance.tokenizedFetch).toHaveBeenCalledWith(
`http://${thoughtSpotHost}${authService.EndPoints.EXECUTE_TML}`,
{
credentials: 'include',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'x-requested-by': 'ThoughtSpot',
}),
body: JSON.stringify(data),
method: 'POST',
},
);
});
test('should log an error when executing TML fails', async () => {
jest.spyOn(window, 'fetch').mockRejectedValue(new Error('Network error'));
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
const data: base.executeTMLInput = {
metadata_tmls: ['{"liveboard":{"name":"Parameters Liveboard"}}'],
import_policy: 'PARTIAL',
create_new: false,
};
try {
await index.executeTML(data);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('Network error');
}
});
test('should reject with an error when sanity check fails', async () => {
const error = new Error('ThoughtSpot host not provided');
const data: base.executeTMLInput = {
metadata_tmls: ['{"liveboard":{"name":"Parameters Liveboard"}}'],
import_policy: 'PARTIAL',
create_new: false,
};
base.reset();
try {
await index.executeTML(data);
} catch (err) {
expect(err).toEqual(error);
}
});
test('should call the exportTML API and export TML', async () => {
jest.spyOn(tokenizedFetchInstance, 'tokenizedFetch').mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValue({}),
});
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
const data: base.exportTMLInput = {
metadata: [{ identifier: 'f5728369-cf02-4953-87ab-a6cac691e360' }],
export_associated: false,
export_fqn: false,
edoc_format: 'YAML',
};
await index.exportTML(data);
expect(tokenizedFetchInstance.tokenizedFetch).toHaveBeenCalledWith(
`http://${thoughtSpotHost}${authService.EndPoints.EXPORT_TML}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'x-requested-by': 'ThoughtSpot',
},
body: JSON.stringify(data),
method: 'POST',
},
);
});
test('should log an error when exeporting TML fails', async () => {
jest.spyOn(window, 'fetch').mockRejectedValue(new Error('Network error'));
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
const data: base.exportTMLInput = {
metadata: [{ identifier: 'f5728369-cf02-4953-87ab-a6cac691e360' }],
export_associated: false,
export_fqn: false,
edoc_format: 'YAML',
};
try {
await index.exportTML(data);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('Network error');
}
});
test('Should add the prefetch iframe when prefetch is called. Should remove it once init is called.', async () => {
const url = 'https://10.87.90.95/?embedApp=true';
index.init({
thoughtSpotHost: url,
authType: index.AuthType.None,
callPrefetch: true,
});
expect(getAllIframeEl().length).toBe(1);
const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>('.prefetchIframe');
expect(prefetchIframe.length).toBe(1);
const firstIframe = <HTMLIFrameElement>prefetchIframe[0];
expect(firstIframe.src).toBe(url);
expect(firstIframe.style.width).toBe('0px');
expect(firstIframe.classList.contains('prefetchIframeNum-0')).toBe(true);
});
test('Should add the prefetch iframe when prefetch is called with multiple options', async () => {
const url = 'https://10.87.90.95/';
const searchUrl = `${url}v2/?embedApp=true#/embed/answer`;
const liveboardUrl = `${url}?embedApp=true`;
index.prefetch(url, [
index.PrefetchFeatures.SearchEmbed,
index.PrefetchFeatures.LiveboardEmbed,
]);
expect(getAllIframeEl().length).toBe(2);
const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>('.prefetchIframe');
expect(prefetchIframe.length).toBe(2);
const firstIframe = <HTMLIFrameElement>prefetchIframe[0];
expect(firstIframe.src).toBe(searchUrl);
const secondIframe = <HTMLIFrameElement>prefetchIframe[1];
expect(secondIframe.src).toBe(liveboardUrl);
});
test('Should add the prefetch iframe with additionalFlags', async () => {
const url = 'https://10.87.90.95/';
const searchUrl = `${url}v2/?embedApp=true&flag2=bool&flag3=block&flag1=true#/embed/answer`;
const liveboardUrl = `${url}?embedApp=true&flag2=bool&flag3=block&flag1=true`;
base.init({
thoughtSpotHost: url,
authType: index.AuthType.None,
additionalFlags: {
flag2: 'bar',
flag3: 'block',
},
});
index.prefetch(url, [
index.PrefetchFeatures.SearchEmbed,
index.PrefetchFeatures.LiveboardEmbed,
],
{ flag1: true, flag2: 'bool' });
expect(getAllIframeEl().length).toBe(2);
const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>('.prefetchIframe');
expect(prefetchIframe.length).toBe(2);
const firstIframe = <HTMLIFrameElement>prefetchIframe[0];
expect(firstIframe.src).toBe(searchUrl);
const secondIframe = <HTMLIFrameElement>prefetchIframe[1];
expect(secondIframe.src).toBe(liveboardUrl);
});
test('Should add the prefetch iframe with additionalFlags for prefetch from init', async () => {
const url = 'https://10.87.90.95/';
const prefetchUrl = `${url}?embedApp=true&flag2=bar&flag3=block`;
base.init({
thoughtSpotHost: url,
authType: index.AuthType.None,
additionalFlags: {
flag2: 'bar',
flag3: 'block',
},
callPrefetch: true,
});
expect(getAllIframeEl().length).toBe(1);
const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>('.prefetchIframe');
expect(prefetchIframe.length).toBe(1);
const firstIframe = <HTMLIFrameElement>prefetchIframe[0];
expect(firstIframe.src).toBe(prefetchUrl);
});
test('Should not generate a prefetch iframe when url is empty string', async () => {
const url = '';
index.prefetch(url);
expect(getAllIframeEl().length).toBe(0);
const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>('.prefetchIframe');
expect(prefetchIframe.length).toBe(0);
});
test('Should not call prefetch inside init when callPrefetch is set to false', async () => {
const prefetch = jest.spyOn(index, 'prefetch');
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
callPrefetch: false,
});
expect(prefetch).toHaveBeenCalledTimes(0);
});
test('Sets the disableLoginRedirect param when autoLogin is true', async () => {
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
const tsEmbed = new index.AppEmbed(getRootEl(), {});
await tsEmbed.render();
await executeAfterWait(() => {
expect(getIFrameSrc()).toContain('disableLoginRedirect=true');
});
});
test('handleAuth notifies for SDK auth failure', (done) => {
jest.spyOn(auth, 'authenticate').mockResolvedValue(false);
const authEmitter = index.init({
thoughtSpotHost,
authType: index.AuthType.Basic,
username: 'test',
password: 'test',
});
authEmitter.on(auth.AuthStatus.FAILURE, (reason) => {
expect(reason).toBe(auth.AuthFailureType.SDK);
done();
});
});
test('handleAuth notifies for SDK auth success', (done) => {
jest.spyOn(auth, 'authenticate').mockResolvedValue(true);
const failureCallback = jest.fn();
const authEmitter = index.init({
thoughtSpotHost,
authType: index.AuthType.Basic,
username: 'test',
password: 'test',
});
authEmitter.on(auth.AuthStatus.FAILURE, failureCallback);
authEmitter.on(auth.AuthStatus.SDK_SUCCESS, (...args) => {
expect(failureCallback).not.toBeCalled();
expect(args.length).toBe(0);
done();
});
});
test('Logout method should disable autoLogin', () => {
jest.spyOn(window, 'fetch').mockResolvedValueOnce({
type: 'opaque',
});
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
index.logout();
expect(window.fetch).toHaveBeenCalledWith(
`http://${thoughtSpotHost}${authService.EndPoints.LOGOUT}`,
{
credentials: 'include',
headers: {
'x-requested-by': 'ThoughtSpot',
},
method: 'POST',
},
);
expect(embedConfigInstance.getEmbedConfig().autoLogin).toBe(false);
});
test('Logout method should reset caches', async () => {
jest.spyOn(tokenAuthServices, 'fetchLogoutService').mockResolvedValueOnce({});
jest.spyOn(resetService, 'resetAllCachedServices');
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
autoLogin: true,
});
expect(resetService.resetAllCachedServices).toHaveBeenCalledTimes(1);
await index.logout();
expect(resetService.resetAllCachedServices).toHaveBeenCalledTimes(2);
});
test('config sanity, no ts host', () => {
expect(() => {
index.init({
authType: index.AuthType.None,
} as EmbedConfig);
}).toThrowError();
});
test('config sanity, no username in trusted auth', () => {
expect(() => {
index.init({
authType: index.AuthType.TrustedAuthToken,
thoughtSpotHost,
} as EmbedConfig);
}).toThrowError();
});
test('config sanity, no authEndpoint and getAuthToken', () => {
expect(() => {
index.init({
authType: index.AuthType.TrustedAuthToken,
thoughtSpotHost,
username: 'test',
});
}).toThrowError();
});
test('config backward compat, should assign inPopup when noRedirect is set', () => {
index.init({
authType: index.AuthType.None,
thoughtSpotHost,
noRedirect: true,
});
expect(embedConfigInstance.getEmbedConfig().inPopup).toBe(true);
});
test('config backward compat, should not override inPopup with noRedirect', () => {
index.init({
authType: index.AuthType.None,
thoughtSpotHost,
noRedirect: true,
inPopup: false,
});
expect(embedConfigInstance.getEmbedConfig().inPopup).toBe(false);
});
test('@P0 @SCAL-226935 embedConfig should contain correct value of customCSSUrl when added in init ', async () => {
index.init({
thoughtSpotHost,
authType: index.AuthType.None,
customizations: {
style: {
customCSSUrl: 'test.com',
},
},
});
expect(embedConfigInstance.getEmbedConfig().customizations.style.customCSSUrl).toEqual('test.com');
});
});
describe('Base without init', () => {
test('notify should error when called without init', () => {
base.reset();
jest.spyOn(logger, 'error').mockImplementation(() => undefined);
base.notifyAuthSuccess();
base.notifyAuthFailure(auth.AuthFailureType.SDK);
base.notifyLogout();
base.notifyAuthSDKSuccess();
expect(logger.error).toHaveBeenCalledTimes(4);
});
});
describe('Init tests', () => {
test('clear caches on init', () => {
jest.spyOn(resetService, 'resetAllCachedServices');
base.init({
thoughtSpotHost,
authType: index.AuthType.None,
});
expect(resetService.resetAllCachedServices).toBeCalled();
});
});