Skip to content

Commit

Permalink
Merge pull request #1066 from Microsoft/jwilaby/#941-ABS-deep-linking
Browse files Browse the repository at this point in the history
#941 - added ABS deep linking in the endpoints expolorer
  • Loading branch information
Justin Wilaby committed Oct 31, 2018
2 parents a12e95c + 969e983 commit 057dc8b
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 44 deletions.
7 changes: 4 additions & 3 deletions packages/app/client/src/data/sagas/endpointSagas.spec.ts
Expand Up @@ -81,9 +81,10 @@ describe('The endpoint sagas', () => {

describe(' openEndpointContextMenu', () => {
const menuItems = [
{ 'id': 'edit', 'label': 'Edit settings' },
{ 'id': 'open', 'label': 'Open in emulator' },
{ 'id': 'forget', 'label': 'Remove' }
{ label: 'Edit settings', id: 'edit' },
{ label: 'Open in emulator', id: 'open' },
{ label: 'Open in portal', id: 'absLink', enabled: jasmine.any(Boolean) },
{ label: 'Remove', id: 'forget' }
];

const { DisplayContextMenu, ShowMessageBox } = SharedConstants.Commands.Electron;
Expand Down
37 changes: 29 additions & 8 deletions packages/app/client/src/data/sagas/endpointSagas.ts
Expand Up @@ -31,11 +31,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { IEndpointService } from 'botframework-config/lib/schema';
import { SharedConstants } from '@bfemulator/app-shared';
import { IBotService, IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { call, ForkEffect, takeEvery, takeLatest } from 'redux-saga/effects';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { openServiceDeepLink } from '../action/connectedServiceActions';
import {
EndpointEditorPayload,
EndpointServiceAction,
Expand All @@ -44,28 +46,43 @@ import {
OPEN_ENDPOINT_CONTEXT_MENU,
OPEN_ENDPOINT_DEEP_LINK
} from '../action/endpointServiceActions';
import { SharedConstants } from '@bfemulator/app-shared';
import { RootState } from '../store';

const getConnectedAbs = (state: RootState, endpointAppId: string) => {
return (state.bot.activeBot.services || []).find(service => {
return service.type === ServiceTypes.Bot && (service as IBotService).appId === endpointAppId;
});
};

function* launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
const { endpointEditorComponent, endpointService = {} } = action.payload;
const servicesToUpdate = yield DialogService
.showDialog<ComponentClass<any>>(endpointEditorComponent, {
endpointService
});
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>
(endpointEditorComponent, { endpointService });

if (servicesToUpdate) {
const { AddOrUpdateService, RemoveService } = SharedConstants.Commands.Bot;
let i = servicesToUpdate.length;
while (i--) {
const service = servicesToUpdate[i];
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.AddOrUpdateService, service.type, service);
let shouldBeRemoved = false;
if (service.type === ServiceTypes.Bot) {
// Since we could end up with an invalid ABS
// naively validate and remove it if all fields are missing
const { serviceName, resourceGroup, subscriptionId, tenantId } = service as IBotService;
shouldBeRemoved = !serviceName && !resourceGroup && !subscriptionId && !tenantId;
}
yield CommandServiceImpl.remoteCall(shouldBeRemoved ? RemoveService : AddOrUpdateService, service.type, service);
}
}
}

function* openEndpointContextMenu(action: EndpointServiceAction<EndpointServicePayload | EndpointEditorPayload>)
: IterableIterator<any> {
const connectedAbs = yield select<RootState, string>(getConnectedAbs, action.payload.endpointService.appId);
const menuItems = [
{ label: 'Edit settings', id: 'edit' },
{ label: 'Open in emulator', id: 'open' },
{ label: 'Open in portal', id: 'absLink', enabled: !!connectedAbs },
{ label: 'Remove', id: 'forget' }
];
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
Expand All @@ -79,6 +96,10 @@ function* openEndpointContextMenu(action: EndpointServiceAction<EndpointServiceP
yield* openEndpointDeepLink(action);
break;

case 'absLink':
yield put(openServiceDeepLink(connectedAbs));
break;

case 'forget':
yield* removeEndpointServiceFromActiveBot(action.payload.endpointService);
break;
Expand Down
Expand Up @@ -56,7 +56,7 @@ export const DialogService = new class implements DialogService {
*
* Ex. DialogService.showDialog(PasswordPromptDialog).then(pw => // do something with password from dialog)
*/
showDialog<T extends ComponentClass | StatelessComponent>(dialog: T, props: {} = {}): Promise<any> {
showDialog<T extends ComponentClass | StatelessComponent, R = any>(dialog: T, props: {} = {}): Promise<R> {
if (!this._hostElement) {
return new Promise((resolve) => resolve(null));
}
Expand Down
88 changes: 73 additions & 15 deletions packages/app/main/src/botHelpers.spec.ts
Expand Up @@ -13,7 +13,7 @@
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// Software), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
Expand All @@ -22,7 +22,7 @@
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
Expand All @@ -47,7 +47,8 @@ jest.mock('./botData/store', () => ({
botFiles: [
{ path: 'path1', displayName: 'name1', secret: '' },
{ path: 'path2', displayName: 'name2', secret: '' },
{ path: 'path3', displayName: 'name3', secret: '' }
{ path: 'path3', displayName: 'name3', secret: '' },
{ path: 'path4', displayName: 'name4', secret: 'ffsafsdfdsa' }
]
}
}),
Expand Down Expand Up @@ -75,11 +76,12 @@ import {
removeBotFromList,
cloneBot,
toSavableBot,
promptForSecretAndRetry
promptForSecretAndRetry, loadBotWithRetry, saveBot
} from './botHelpers';

describe('botHelpers tests', () => {
test('getActiveBot()', () => {
describe('The botHelpers', () => {

it('getActiveBot() should retrieve the active bot', () => {
let activeBot = getActiveBot();
expect(activeBot).toEqual({
name: 'someBot',
Expand All @@ -90,31 +92,32 @@ describe('botHelpers tests', () => {
});
});

test('getBotInfoByPath()', () => {
it('getBotInfoByPath() should get the bot info matching the specified path', () => {
const info = getBotInfoByPath('path2');
expect(info).toEqual({ path: 'path2', displayName: 'name2', secret: '' });
});

test('pathExistsInRecentBots()', () => {
it('pathExistsInRecentBots() should determine if the specified path exists in the recent bot list', () => {
const pathExists = pathExistsInRecentBots('path1');
expect(pathExists).toBe(true);
});

test(`removeBotFromList()`, () => {
it(`removeBotFromList() should remove the bot from the list based on the specified path`, async () => {
const spy = jest.spyOn(mainWindow.commandService, 'remoteCall');
removeBotFromList('path3');
await removeBotFromList('path3');

// should have sync'd up list with remaining 2 bot entries (3rd was removed)
expect(spy).toHaveBeenCalledWith(
SharedConstants.Commands.Bot.SyncBotList,
[
{ path: 'path1', displayName: 'name1', secret: '' },
{ path: 'path2', displayName: 'name2', secret: '' }
{ path: 'path2', displayName: 'name2', secret: '' },
{displayName: 'name4', path: 'path4', secret: 'ffsafsdfdsa'}
]
);
});

test('cloneBot()', () => {
it('cloneBot() should clone the specified bot as expected', () => {
const bot1 = null;
expect(cloneBot(bot1)).toBe(null);

Expand All @@ -130,9 +133,9 @@ describe('botHelpers tests', () => {
expect(cloneBot(bot2)).toEqual(bot2);
});

test('toSavableBot()', () => {
it('toSavableBot() should convert the specified bot to a savable instance', () => {
const bot1 = null;
expect(() => toSavableBot(bot1)).toThrowError('Cannot convert falsy bot to savable bot.');
expect(() => toSavableBot(bot1)).toThrowError('Cannot convert null bot to savable bot.');

const bot2: BotConfigWithPath = BotConfigWithPathImpl.fromJSON({
version: '',
Expand All @@ -157,7 +160,7 @@ describe('botHelpers tests', () => {
expect(savableBot.padlock).not.toEqual(secret);
});

test('promptForSecretAndRetry()', async () => {
it('promptForSecretAndRetry() should prompt the user for the bot secret', async () => {
mainWindow.commandService.remoteCall = jest.fn()
.mockImplementationOnce(() => Promise.resolve(null))
.mockImplementation(() => Promise.resolve('secret'));
Expand All @@ -173,4 +176,59 @@ describe('botHelpers tests', () => {
expect(e.code).toBe('ENOENT');
}
});

it('saveBot() should save a bot', async () => {
let saved = false;
const fromJSONSpy = jest.spyOn(BotConfiguration, 'fromJSON').mockReturnValue({
internal: {},
validateSecret: () => true,
save: async () => {
saved = true;
}
});
await saveBot({
path: 'path4'
} as any);
expect(saved).toBeTruthy();
});

describe('loadBotWithRetry()', () => {

it('should prompt the user for the secret and retry if no secret was given for an encrypted bot', async () => {
const botConfigLoadSpy = jest.spyOn(BotConfiguration, 'load').mockResolvedValue({ padlock: '55sdgfd' });
const result = await loadBotWithRetry('path');
expect(botConfigLoadSpy).toHaveBeenCalledWith('path', undefined);

expect(result).toEqual({
description: '',
name: '',
overrides: null,
padlock: '55sdgfd',
path: 'path',
services: [],
version: '2.0'
});
});

it('should update the secret when the specified secret does not match the one on record', async () => {
const botConfigLoadSpy = jest.spyOn(BotConfiguration, 'load').mockResolvedValue({ padlock: 'newSecret' });
const remoteCallSpy = jest.spyOn(mainWindow.commandService, 'remoteCall').mockResolvedValue('newSecret');
const result = await loadBotWithRetry('path1');
expect(botConfigLoadSpy).toHaveBeenCalledWith('path1', undefined);
expect(result).toEqual({
description: '',
name: '',
overrides: null,
padlock: 'newSecret',
path: 'path1',
services: [],
version: '2.0'
});
expect(remoteCallSpy).toHaveBeenCalledWith('bot:list:sync', [
{ displayName: 'name1', path: 'path1', secret: 'newSecret' },
{ displayName: 'name2', path: 'path2', secret: '' },
{ displayName: 'name3', path: 'path3', secret: '' },
{ path: 'path4', displayName: 'name4', secret: 'ffsafsdfdsa' }]);
});
});
});
2 changes: 1 addition & 1 deletion packages/app/main/src/botHelpers.ts
Expand Up @@ -121,7 +121,7 @@ export async function promptForSecretAndRetry(botPath: string): Promise<BotConfi
/** Converts a BotConfigWithPath to a BotConfig */
export function toSavableBot(bot: BotConfigWithPath, secret?: string): BotConfiguration {
if (!bot) {
throw new Error('Cannot convert falsy bot to savable bot.');
throw new Error(`Cannot convert ${'' + bot} bot to savable bot.`);
}
const newBot = BotConfiguration.fromJSON(bot);
(newBot as any).internal.location = bot.path; // Workaround until defect is fixed
Expand Down

0 comments on commit 057dc8b

Please sign in to comment.