Skip to content
This repository has been archived by the owner on Feb 14, 2025. It is now read-only.

SCAL-233969: Add preauth info call, emit Info call success event Iframe load #100

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
9 changes: 8 additions & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
} from './utils/authService';
import { isActiveService } from './utils/authService/tokenizedAuthService';
import { logger } from './utils/logger';
import { getSessionInfo } from './utils/sessionInfoService';
import { getSessionInfo, getPreauthInfo } from './utils/sessionInfoService';
import { ERROR_MESSAGE } from './errors';

// eslint-disable-next-line import/no-mutable-exports
Expand All @@ -42,7 +42,7 @@
}

/**
* Enum for auth status emitted by the emitter returned from {@link init}.

Check warning on line 45 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

The type 'init' is undefined
* @group Authentication / Init
*/
export enum AuthStatus {
Expand All @@ -54,6 +54,11 @@
* Emits when the SDK authenticates successfully
*/
SDK_SUCCESS = 'SDK_SUCCESS',
/**
* @hidden
* Emits when iframe is loaded and session info is available
*/
SESSION_INFO_SUCCESS = 'SESSION_INFO_SUCCESS',
/**
* Emits when the app sends an authentication success message
*/
Expand All @@ -66,13 +71,13 @@
* Emitted when inPopup is true in the SAMLRedirect flow and the
* popup is waiting to be triggered either programmatically
* or by the trigger button.
* @version SDK: 1.19.0

Check warning on line 74 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Invalid JSDoc @Version: "SDK: 1.19.0"
*/
WAITING_FOR_POPUP = 'WAITING_FOR_POPUP',
}

/**
* Event emitter returned from {@link init}.

Check warning on line 80 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

The type 'init' is undefined
* @group Authentication / Init
*/
export interface AuthEventEmitter {
Expand Down Expand Up @@ -100,7 +105,7 @@
once(event: AuthStatus.SUCCESS, listener: (sessionInfo: any) => void): this;
/**
* Trigger an event on the emitter returned from init.
* @param {@link AuthEvent}

Check warning on line 108 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Syntax error in type: @link AuthEvent
*/
emit(event: AuthEvent, ...args: any[]): boolean;
/**
Expand All @@ -119,7 +124,7 @@
}

/**
* Events which can be triggered on the emitter returned from {@link init}.

Check warning on line 127 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

The type 'init' is undefined
* @group Authentication / Init
*/
export enum AuthEvent {
Expand All @@ -132,7 +137,7 @@

let authEE: EventEmitter<AuthStatus | AuthEvent>;

/**

Check warning on line 140 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @returns declaration
*
*/
export function getAuthEE(): EventEmitter<AuthStatus | AuthEvent> {
Expand All @@ -141,7 +146,7 @@

/**
*
* @param eventEmitter

Check warning on line 149 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "eventEmitter" description

Check warning on line 149 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "eventEmitter" type
*/
export function setAuthEE(eventEmitter: EventEmitter<AuthStatus | AuthEvent>): void {
authEE = eventEmitter;
Expand All @@ -167,6 +172,7 @@
return;
}
try {
getPreauthInfo();
const sessionInfo = await getSessionInfo();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace this getSessionInfo with the getPreauthInfo here? Better to avoid multiple api calls.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or update the getSessionInfo to call getPreauthInfo internally and replace the dependency on old /info call

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is decided with TSE to keep both APIs as older customers won't have preauth info API. Check more detail here - https://thoughtspot.slack.com/archives/C082G97N8BZ/p1732810938126219

Screenshot 2025-02-13 at 9 17 39 AM

authEE.emit(AuthStatus.SUCCESS, sessionInfo);
} catch (e) {
Expand All @@ -176,7 +182,7 @@

/**
*
* @param failureType

Check warning on line 185 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "failureType" description

Check warning on line 185 in src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Missing JSDoc @param "failureType" type
*/
export function notifyAuthFailure(failureType: AuthFailureType): void {
if (!authEE) {
Expand Down Expand Up @@ -223,6 +229,7 @@
*/
export async function postLoginService(): Promise<void> {
try {
getPreauthInfo();
const sessionInfo = await getSessionInfo();
ajeet-lakhani-ts marked this conversation as resolved.
Show resolved Hide resolved
releaseVersion = sessionInfo.releaseVersion;
const embedConfig = getEmbedConfig();
Expand Down
96 changes: 95 additions & 1 deletion src/embed/ts-embed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ import * as mixpanelInstance from '../mixpanel-service';
import * as authInstance from '../auth';
import * as baseInstance from './base';
import { MIXPANEL_EVENT } from '../mixpanel-service';
import * as authService from '../utils/authService/authService';
import * as authService from '../utils/authService';
import { logger } from '../utils/logger';
import { version } from '../../package.json';
import { HiddenActionItemByDefaultForSearchEmbed } from './search';
import { processTrigger } from '../utils/processTrigger';
import { UIPassthroughEvent } from './hostEventClient/contracts';
import * as sessionInfoService from '../utils/sessionInfoService';

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

Expand Down Expand Up @@ -1109,6 +1110,99 @@ describe('Unit test case for ts embed', () => {
});
});

describe('Trigger infoSuccess event on iframe load', () => {
beforeAll(() => {
jest.clearAllMocks();
init({
thoughtSpotHost,
authType: AuthType.None,
loginFailedMessage: 'Failed to Login',
});
});

const setup = async (isLoggedIn = false, overrideOrgId: number | undefined = undefined) => {
jest.spyOn(window, 'addEventListener').mockImplementationOnce(
(event, handler, options) => {
handler({
data: {
type: 'xyz',
},
ports: [3000],
source: null,
});
},
);
mockProcessTrigger.mockResolvedValueOnce({ session: 'test' });
// resetCachedPreauthInfo();
let mockGetPreauthInfo = null;

if (overrideOrgId) {
mockGetPreauthInfo = jest.spyOn(sessionInfoService, 'getPreauthInfo').mockImplementation(jest.fn());
}

const mockPreauthInfoFetch = jest.spyOn(authService, 'fetchPreauthInfoService').mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
json: async () => ({
info: {
configInfo: {
mixpanelConfig: {
devSdkKey: 'devSdkKey',
},
},
userGUID: 'userGUID',
},
}), // Mock JSON response
});
const iFrame: any = document.createElement('div');
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(isLoggedIn);
const tsEmbed = new SearchEmbed(getRootEl(), {
overrideOrgId,
});
iFrame.contentWindow = {
postMessage: jest.fn(),
};
tsEmbed.on(EmbedEvent.CustomAction, jest.fn());
jest.spyOn(iFrame, 'addEventListener').mockImplementationOnce(
(event, handler, options) => {
handler({});
},
);
jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame);
await tsEmbed.render();

return {
mockPreauthInfoFetch,
mockGetPreauthInfo,
iFrame,
};
};

test('should call InfoSuccess Event on preauth call success', async () => {
const {
mockPreauthInfoFetch,
iFrame,
} = await setup(true);
expect(mockPreauthInfoFetch).toHaveBeenCalledTimes(1);

await executeAfterWait(() => {
expect(mockProcessTrigger).toHaveBeenCalledWith(
iFrame,
HostEvent.InfoSuccess,
'http://tshost',
expect.objectContaining({ info: expect.any(Object) }),
);
});
});

test('should not call InfoSuccess Event if overrideOrgId is true', async () => {
const {
mockGetPreauthInfo,
} = await setup(true, 123);
expect(mockGetPreauthInfo).toHaveBeenCalledTimes(0);
});
});

describe('when thoughtSpotHost have value and authPromise return error', () => {
beforeAll(() => {
init({
Expand Down
9 changes: 9 additions & 0 deletions src/embed/ts-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
import { AuthFailureType } from '../auth';
import { getEmbedConfig } from './embedConfig';
import { ERROR_MESSAGE } from '../errors';
import { getPreauthInfo } from '../utils/sessionInfoService';
import { HostEventClient } from './hostEventClient/host-event-client';

const { version } = pkgInfo;
Expand Down Expand Up @@ -708,6 +709,14 @@ export class TsEmbed {
elHeight: this.iFrame.clientHeight,
timeTookToLoad: loadTimestamp - initTimestamp,
});
// Disable preauth info fetch for perUrlOrg is enabled
if (this.viewConfig.overrideOrgId === undefined) {
getPreauthInfo().then((data) => {
if (data?.info) {
this.trigger(HostEvent.InfoSuccess, data);
ajeet-lakhani-ts marked this conversation as resolved.
Show resolved Hide resolved
}
});
}
});
this.iFrame.addEventListener('error', () => {
nextInQueue();
Expand Down
13 changes: 11 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3234,8 +3234,17 @@ export enum HostEvent {
*/
UpdatePersonalisedView = 'UpdatePersonalisedView',
/**
* Triggers the action to get the current view of the Liveboard.
* @version SDK: 1.36.0 | ThoughtSpot: 10.6.0.cl
* @hidden
* Notify when info call is completed successfully
* ```js
* liveboardEmbed.trigger(HostEvent.InfoSuccess, data);
*```
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
*/
InfoSuccess = 'InfoSuccess',
/**
* Triggers the action to get the current view of the liveboard
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
*/
SaveAnswer = 'saveAnswer',
/**
Expand Down
1 change: 1 addition & 0 deletions src/utils/authService/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { logger } from '../logger';
export const EndPoints = {
AUTH_VERIFICATION: '/callosum/v1/session/info',
SESSION_INFO: '/callosum/v1/session/info',
PREAUTH_INFO: '/prism/preauth/info',
SAML_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/saml/login?targetURLPath=${targetUrl}`,
OIDC_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/oidc/login?targetURLPath=${targetUrl}`,
TOKEN_LOGIN: '/callosum/v1/session/login/token',
Expand Down
6 changes: 5 additions & 1 deletion src/utils/authService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ export {
fetchBasicAuthService,
verifyTokenService,
} from './authService';
export { fetchLogoutService, fetchSessionInfoService } from './tokenizedAuthService';
export {
fetchLogoutService,
fetchSessionInfoService,
fetchPreauthInfoService,
} from './tokenizedAuthService';
68 changes: 66 additions & 2 deletions src/utils/authService/tokenizedAuthService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import * as tokenizedFetchModule from '../../tokenizedFetch';
import { isActiveService } from './tokenizedAuthService';
import { isActiveService, fetchSessionInfoService, fetchPreauthInfoService } from './tokenizedAuthService';
import { logger } from '../logger';
import { EndPoints } from './authService';

const thoughtspotHost = 'http://thoughtspotHost';

describe('tokenizedAuthService', () => {
test('isActiveService is fetch returns ok', async () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
test('isActiveService if fetch returns ok', async () => {
jest.spyOn(tokenizedFetchModule, 'tokenizedFetch').mockResolvedValueOnce({
ok: true,
});
Expand Down Expand Up @@ -34,3 +41,60 @@ describe('tokenizedAuthService', () => {
expect(logger.warn).toHaveBeenCalled();
});
});

describe('fetchPreauthInfoService', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

test('fetchPreauthInfoService if fetch returns ok', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch
.mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
status: 200,
statusText: 'Ok',
json: jest.fn().mockResolvedValue({
info: {
configInfo: {
mixpanelConfig: {
devSdkKey: 'devSdkKey',
},
},
userGUID: 'userGUID',
},
}),
});

const result = await fetchPreauthInfoService(thoughtspotHost);
const response = await result.json();

expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenNthCalledWith(1, `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
expect(response).toHaveProperty('info');
});
it('fetchPreauthInfoService if fetch fails', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue('Internal Server Error'),
});

try {
await fetchPreauthInfoService(thoughtspotHost);
} catch (e) {
expect(e.message).toContain(`Failed to fetch ${thoughtspotHost}${EndPoints.PREAUTH_INFO}`);
}
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(`${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
});
});
26 changes: 26 additions & 0 deletions src/utils/authService/tokenizedAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ function tokenizedFailureLoggedFetch(url: string, options: RequestInit = {}): Pr
});
}

/**
* Fetches the session info from the ThoughtSpot server.
* @param thoughtspotHost
* @returns {Promise<any>}
* @example
* ```js
* const response = await sessionInfoService();
* ```
*/
export async function fetchPreauthInfoService(thoughtspotHost: string): Promise<any> {
const sessionInfoPath = `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`;
const handleError = (e: any) => {
const error: any = new Error(`Failed to fetch auth info: ${e.message || e.statusText}`);
error.status = e.status; // Attach the status code to the error object
throw error;
};

try {
const response = await tokenizedFailureLoggedFetch(sessionInfoPath);
return response;
} catch (e) {
handleError(e);
return null;
}
}

/**
* Fetches the session info from the ThoughtSpot server.
* @param thoughtspotHost
Expand Down
1 change: 1 addition & 0 deletions src/utils/processTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function postIframeMessage(
thoughtSpotHost: string,
channel?: MessageChannel,
) {
// console.log('=====> postIframeMessage', iFrame, message);
ajeet-lakhani-ts marked this conversation as resolved.
Show resolved Hide resolved
return iFrame.contentWindow.postMessage(message, thoughtSpotHost, [channel?.port2]);
}

Expand Down
Loading
Loading