Skip to content

Commit 1f4a8df

Browse files
SCAL-233969: Add preauth info call, emit Info call success event Iframe load (#100)
1 parent a139c14 commit 1f4a8df

10 files changed

Lines changed: 312 additions & 10 deletions

File tree

src/auth.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from './utils/authService';
1616
import { isActiveService } from './utils/authService/tokenizedAuthService';
1717
import { logger } from './utils/logger';
18-
import { getSessionInfo } from './utils/sessionInfoService';
18+
import { getSessionInfo, getPreauthInfo } from './utils/sessionInfoService';
1919
import { ERROR_MESSAGE } from './errors';
2020

2121
// eslint-disable-next-line import/no-mutable-exports
@@ -55,6 +55,11 @@ export enum AuthStatus {
5555
* Emits when the SDK authenticates successfully
5656
*/
5757
SDK_SUCCESS = 'SDK_SUCCESS',
58+
/**
59+
* @hidden
60+
* Emits when iframe is loaded and session info is available
61+
*/
62+
SESSION_INFO_SUCCESS = 'SESSION_INFO_SUCCESS',
5863
/**
5964
* Emits when the app sends an authentication success message
6065
*/
@@ -168,6 +173,7 @@ export async function notifyAuthSuccess(): Promise<void> {
168173
return;
169174
}
170175
try {
176+
getPreauthInfo();
171177
const sessionInfo = await getSessionInfo();
172178
authEE.emit(AuthStatus.SUCCESS, sessionInfo);
173179
} catch (e) {
@@ -224,6 +230,7 @@ async function isLoggedIn(thoughtSpotHost: string): Promise<boolean> {
224230
*/
225231
export async function postLoginService(): Promise<void> {
226232
try {
233+
getPreauthInfo();
227234
const sessionInfo = await getSessionInfo();
228235
releaseVersion = sessionInfo.releaseVersion;
229236
const embedConfig = getEmbedConfig();

src/embed/ts-embed.spec.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,13 @@ import * as mixpanelInstance from '../mixpanel-service';
5151
import * as authInstance from '../auth';
5252
import * as baseInstance from './base';
5353
import { MIXPANEL_EVENT } from '../mixpanel-service';
54-
import * as authService from '../utils/authService/authService';
54+
import * as authService from '../utils/authService';
5555
import { logger } from '../utils/logger';
5656
import { version } from '../../package.json';
5757
import { HiddenActionItemByDefaultForSearchEmbed } from './search';
5858
import { processTrigger } from '../utils/processTrigger';
5959
import { UIPassthroughEvent } from './hostEventClient/contracts';
60+
import * as sessionInfoService from '../utils/sessionInfoService';
6061

6162
jest.mock('../utils/processTrigger');
6263

@@ -1147,6 +1148,99 @@ describe('Unit test case for ts embed', () => {
11471148
});
11481149
});
11491150

1151+
describe('Trigger infoSuccess event on iframe load', () => {
1152+
beforeAll(() => {
1153+
jest.clearAllMocks();
1154+
init({
1155+
thoughtSpotHost,
1156+
authType: AuthType.None,
1157+
loginFailedMessage: 'Failed to Login',
1158+
});
1159+
});
1160+
1161+
const setup = async (isLoggedIn = false, overrideOrgId: number | undefined = undefined) => {
1162+
jest.spyOn(window, 'addEventListener').mockImplementationOnce(
1163+
(event, handler, options) => {
1164+
handler({
1165+
data: {
1166+
type: 'xyz',
1167+
},
1168+
ports: [3000],
1169+
source: null,
1170+
});
1171+
},
1172+
);
1173+
mockProcessTrigger.mockResolvedValueOnce({ session: 'test' });
1174+
// resetCachedPreauthInfo();
1175+
let mockGetPreauthInfo = null;
1176+
1177+
if (overrideOrgId) {
1178+
mockGetPreauthInfo = jest.spyOn(sessionInfoService, 'getPreauthInfo').mockImplementation(jest.fn());
1179+
}
1180+
1181+
const mockPreauthInfoFetch = jest.spyOn(authService, 'fetchPreauthInfoService').mockResolvedValueOnce({
1182+
ok: true,
1183+
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
1184+
json: async () => ({
1185+
info: {
1186+
configInfo: {
1187+
mixpanelConfig: {
1188+
devSdkKey: 'devSdkKey',
1189+
},
1190+
},
1191+
userGUID: 'userGUID',
1192+
},
1193+
}), // Mock JSON response
1194+
});
1195+
const iFrame: any = document.createElement('div');
1196+
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(isLoggedIn);
1197+
const tsEmbed = new SearchEmbed(getRootEl(), {
1198+
overrideOrgId,
1199+
});
1200+
iFrame.contentWindow = {
1201+
postMessage: jest.fn(),
1202+
};
1203+
tsEmbed.on(EmbedEvent.CustomAction, jest.fn());
1204+
jest.spyOn(iFrame, 'addEventListener').mockImplementationOnce(
1205+
(event, handler, options) => {
1206+
handler({});
1207+
},
1208+
);
1209+
jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame);
1210+
await tsEmbed.render();
1211+
1212+
return {
1213+
mockPreauthInfoFetch,
1214+
mockGetPreauthInfo,
1215+
iFrame,
1216+
};
1217+
};
1218+
1219+
test('should call InfoSuccess Event on preauth call success', async () => {
1220+
const {
1221+
mockPreauthInfoFetch,
1222+
iFrame,
1223+
} = await setup(true);
1224+
expect(mockPreauthInfoFetch).toHaveBeenCalledTimes(1);
1225+
1226+
await executeAfterWait(() => {
1227+
expect(mockProcessTrigger).toHaveBeenCalledWith(
1228+
iFrame,
1229+
HostEvent.InfoSuccess,
1230+
'http://tshost',
1231+
expect.objectContaining({ info: expect.any(Object) }),
1232+
);
1233+
});
1234+
});
1235+
1236+
test('should not call InfoSuccess Event if overrideOrgId is true', async () => {
1237+
const {
1238+
mockGetPreauthInfo,
1239+
} = await setup(true, 123);
1240+
expect(mockGetPreauthInfo).toHaveBeenCalledTimes(0);
1241+
});
1242+
});
1243+
11501244
describe('when thoughtSpotHost have value and authPromise return error', () => {
11511245
beforeAll(() => {
11521246
init({

src/embed/ts-embed.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
import { AuthFailureType } from '../auth';
7171
import { getEmbedConfig } from './embedConfig';
7272
import { ERROR_MESSAGE } from '../errors';
73+
import { getPreauthInfo } from '../utils/sessionInfoService';
7374
import { HostEventClient } from './hostEventClient/host-event-client';
7475

7576
const { version } = pkgInfo;
@@ -244,6 +245,24 @@ export class TsEmbed {
244245
return null;
245246
}
246247

248+
/**
249+
* Checks if preauth cache is enabled
250+
* from the view config and embed config
251+
* @returns boolean
252+
*/
253+
private isPreAuthCacheEnabled() {
254+
// Disable preauth cache when:
255+
// 1. overrideOrgId is present since:
256+
// - cached auth info would be for wrong org
257+
// - info call response changes for each different overrideOrgId
258+
// 2. disablePreauthCache is explicitly set to true
259+
const isDisabled = (
260+
this.viewConfig.overrideOrgId !== undefined
261+
|| this.embedConfig.disablePreauthCache === true
262+
);
263+
return !isDisabled;
264+
}
265+
247266
/**
248267
* fix for ts7.sep.cl
249268
* will be removed for ts7.oct.cl
@@ -586,6 +605,10 @@ export class TsEmbed {
586605
queryParams[Param.OverrideOrgId] = overrideOrgId;
587606
}
588607

608+
if (this.isPreAuthCacheEnabled()) {
609+
queryParams[Param.preAuthCache] = true;
610+
}
611+
589612
queryParams[Param.OverrideNativeConsole] = true;
590613
queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
591614

@@ -722,6 +745,14 @@ export class TsEmbed {
722745
elHeight: this.iFrame.clientHeight,
723746
timeTookToLoad: loadTimestamp - initTimestamp,
724747
});
748+
// Send info event if preauth cache is enabled
749+
if (this.isPreAuthCacheEnabled()) {
750+
getPreauthInfo().then((data) => {
751+
if (data?.info) {
752+
this.trigger(HostEvent.InfoSuccess, data);
753+
}
754+
});
755+
}
725756
});
726757
this.iFrame.addEventListener('error', () => {
727758
nextInQueue();

src/react/index.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('React Components', () => {
5656
),
5757
).toBe(true);
5858
expect(getIFrameSrc(container)).toBe(
59-
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22,%22editACopy%22,%22saveAsView%22,%22updateTSL%22,%22editTSL%22,%22onDeleteAnswer%22]&overrideConsoleLogs=true&clientLogLevel=ERROR&enableDataPanelV2=false&dataSourceMode=hide&useLastSelectedSources=false&isSearchEmbed=true&collapseSearchBarInitially=true&enableCustomColumnGroups=false&dataPanelCustomGroupsAccordionInitialState=EXPAND_ALL#/embed/answer`,
59+
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22,%22editACopy%22,%22saveAsView%22,%22updateTSL%22,%22editTSL%22,%22onDeleteAnswer%22]&preAuthCache=true&overrideConsoleLogs=true&clientLogLevel=ERROR&enableDataPanelV2=false&dataSourceMode=hide&useLastSelectedSources=false&isSearchEmbed=true&collapseSearchBarInitially=true&enableCustomColumnGroups=false&dataPanelCustomGroupsAccordionInitialState=EXPAND_ALL#/embed/answer`,
6060
);
6161
});
6262

@@ -230,7 +230,7 @@ describe('React Components', () => {
230230
),
231231
).toBe(true);
232232
expect(getIFrameSrc(container)).toBe(
233-
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22]&overrideConsoleLogs=true&clientLogLevel=ERROR&dataSources=[%22test%22]&searchTokenString=%5Brevenue%5D&executeSearch=true&useLastSelectedSources=false&isSearchEmbed=true#/embed/search-bar-embed`,
233+
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22]&preAuthCache=true&overrideConsoleLogs=true&clientLogLevel=ERROR&dataSources=[%22test%22]&searchTokenString=%5Brevenue%5D&executeSearch=true&useLastSelectedSources=false&isSearchEmbed=true#/embed/search-bar-embed`,
234234
);
235235
});
236236
});

src/types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,8 @@ export interface EmbedConfig {
629629
* @version SDK 1.37.0 | ThoughtSpot: 10.7.0.cl
630630
*/
631631
customVariablesForThirdPartyTools?: Record< string, any >;
632+
633+
disablePreauthCache?: boolean;
632634
}
633635

634636
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -3269,8 +3271,17 @@ export enum HostEvent {
32693271
*/
32703272
UpdatePersonalisedView = 'UpdatePersonalisedView',
32713273
/**
3272-
* Triggers the action to get the current view of the Liveboard.
3273-
* @version SDK: 1.36.0 | ThoughtSpot: 10.6.0.cl
3274+
* @hidden
3275+
* Notify when info call is completed successfully
3276+
* ```js
3277+
* liveboardEmbed.trigger(HostEvent.InfoSuccess, data);
3278+
*```
3279+
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
3280+
*/
3281+
InfoSuccess = 'InfoSuccess',
3282+
/**
3283+
* Triggers the action to get the current view of the liveboard
3284+
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
32743285
*/
32753286
SaveAnswer = 'saveAnswer',
32763287
/**
@@ -3427,6 +3438,7 @@ export enum Param {
34273438
OauthPollingInterval = 'oAuthPollingInterval',
34283439
IsForceRedirect = 'isForceRedirect',
34293440
DataSourceId = 'dataSourceId',
3441+
preAuthCache = 'preAuthCache',
34303442
ShowSpotterLimitations = 'showSpotterLimitations',
34313443
}
34323444

src/utils/authService/authService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { logger } from '../logger';
33
export const EndPoints = {
44
AUTH_VERIFICATION: '/callosum/v1/session/info',
55
SESSION_INFO: '/callosum/v1/session/info',
6+
PREAUTH_INFO: '/prism/preauth/info',
67
SAML_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/saml/login?targetURLPath=${targetUrl}`,
78
OIDC_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/oidc/login?targetURLPath=${targetUrl}`,
89
TOKEN_LOGIN: '/callosum/v1/session/login/token',

src/utils/authService/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ export {
66
fetchBasicAuthService,
77
verifyTokenService,
88
} from './authService';
9-
export { fetchLogoutService, fetchSessionInfoService } from './tokenizedAuthService';
9+
export {
10+
fetchLogoutService,
11+
fetchSessionInfoService,
12+
fetchPreauthInfoService,
13+
} from './tokenizedAuthService';

src/utils/authService/tokenizedAuthService.spec.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import * as tokenizedFetchModule from '../../tokenizedFetch';
2-
import { isActiveService } from './tokenizedAuthService';
2+
import { isActiveService, fetchSessionInfoService, fetchPreauthInfoService } from './tokenizedAuthService';
33
import { logger } from '../logger';
4+
import { EndPoints } from './authService';
5+
6+
const thoughtspotHost = 'http://thoughtspotHost';
47

58
describe('tokenizedAuthService', () => {
6-
test('isActiveService is fetch returns ok', async () => {
9+
afterEach(() => {
10+
jest.clearAllMocks();
11+
jest.restoreAllMocks();
12+
});
13+
test('isActiveService if fetch returns ok', async () => {
714
jest.spyOn(tokenizedFetchModule, 'tokenizedFetch').mockResolvedValueOnce({
815
ok: true,
916
});
@@ -34,3 +41,60 @@ describe('tokenizedAuthService', () => {
3441
expect(logger.warn).toHaveBeenCalled();
3542
});
3643
});
44+
45+
describe('fetchPreauthInfoService', () => {
46+
afterEach(() => {
47+
jest.clearAllMocks();
48+
jest.restoreAllMocks();
49+
});
50+
51+
test('fetchPreauthInfoService if fetch returns ok', async () => {
52+
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');
53+
54+
// Mock for fetchPreauthInfoService
55+
mockFetch
56+
.mockResolvedValueOnce({
57+
ok: true,
58+
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
59+
status: 200,
60+
statusText: 'Ok',
61+
json: jest.fn().mockResolvedValue({
62+
info: {
63+
configInfo: {
64+
mixpanelConfig: {
65+
devSdkKey: 'devSdkKey',
66+
},
67+
},
68+
userGUID: 'userGUID',
69+
},
70+
}),
71+
});
72+
73+
const result = await fetchPreauthInfoService(thoughtspotHost);
74+
const response = await result.json();
75+
76+
expect(mockFetch).toHaveBeenCalledTimes(1);
77+
expect(mockFetch).toHaveBeenNthCalledWith(1, `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
78+
expect(response).toHaveProperty('info');
79+
});
80+
it('fetchPreauthInfoService if fetch fails', async () => {
81+
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');
82+
83+
// Mock for fetchPreauthInfoService
84+
mockFetch.mockResolvedValueOnce({
85+
ok: false,
86+
status: 500,
87+
statusText: 'Internal Server Error',
88+
json: jest.fn().mockResolvedValue({}),
89+
text: jest.fn().mockResolvedValue('Internal Server Error'),
90+
});
91+
92+
try {
93+
await fetchPreauthInfoService(thoughtspotHost);
94+
} catch (e) {
95+
expect(e.message).toContain(`Failed to fetch ${thoughtspotHost}${EndPoints.PREAUTH_INFO}`);
96+
}
97+
expect(mockFetch).toHaveBeenCalledTimes(1);
98+
expect(mockFetch).toHaveBeenCalledWith(`${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
99+
});
100+
});

src/utils/authService/tokenizedAuthService.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@ function tokenizedFailureLoggedFetch(url: string, options: RequestInit = {}): Pr
1616
});
1717
}
1818

19+
/**
20+
* Fetches the session info from the ThoughtSpot server.
21+
* @param thoughtspotHost
22+
* @returns {Promise<any>}
23+
* @example
24+
* ```js
25+
* const response = await sessionInfoService();
26+
* ```
27+
*/
28+
export async function fetchPreauthInfoService(thoughtspotHost: string): Promise<any> {
29+
const sessionInfoPath = `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`;
30+
const handleError = (e: any) => {
31+
const error: any = new Error(`Failed to fetch auth info: ${e.message || e.statusText}`);
32+
error.status = e.status; // Attach the status code to the error object
33+
throw error;
34+
};
35+
36+
try {
37+
const response = await tokenizedFailureLoggedFetch(sessionInfoPath);
38+
return response;
39+
} catch (e) {
40+
handleError(e);
41+
return null;
42+
}
43+
}
44+
1945
/**
2046
* Fetches the session info from the ThoughtSpot server.
2147
* @param thoughtspotHost

0 commit comments

Comments
 (0)