UNPKG

@thoughtspot/visual-embed-sdk

Version:
510 lines (473 loc) 18.8 kB
/* 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(); }); });