Remove NGrok. Configure Tunnel URL (#2461)

* Rido/ngrok removal (#1)

* update fileMenu

* add tunnelUrl

* get serviceUrl from settings

* showPort

* rm ngrok settings

* showPort

* rm ngrokdebugger

* rm ngroddebug from editor

* rm ngrokService

* rm nGrokTab

* rm ngrok sagas, actions, styles

* rm ngrok settings

* upd version, and deps

* get server port

* update changelog

* fix lint issues

* PR feedback

* Update packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.tsx

Co-authored-by: Eugene <EOlonov@gmail.com>

* Update tsconfig.json

Co-authored-by: Eugene <EOlonov@gmail.com>

* rm ngroc from tests

---------

Co-authored-by: Eugene <EOlonov@gmail.com>
This commit is contained in:
Rido 2024-09-16 13:02:49 -07:00 коммит произвёл GitHub
Родитель e554dc4231
Коммит ffc00bd09e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
87 изменённых файлов: 107 добавлений и 4742 удалений

Просмотреть файл

@ -1 +1 @@
v10.14.2
v16.13.2

Просмотреть файл

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## v4.15.1 - 2024 - 09 - 12
## Update Tunnels
- [client/main] Remove NGrok dependency. Allow to paste the Tunnel URL in settings to enable remote bots.
## v4.14.1 - 2021 - 11 - 12
## Added
- [main] Bumped `electron` to v13.6.1 in PR [2318](https://github.com/microsoft/BotFramework-Emulator/pull/2318) (fixes electron bug caused by [Let's Encrypt root certificate expiration](https://github.com/electron/electron/issues/31212))

Просмотреть файл

@ -87,7 +87,7 @@
"overrides": {
"minimist@<1.2.6": ">=1.2.6",
"follow-redirects@<1.14.8": ">=1.14.8",
"serialize-javascript@<3.1.0": ">=3.1.0",
"serialize-javascript@<6.0.2": ">=6.0.2",
"postcss@<7.0.36": ">=7.0.36",
"trim-newlines@<3.0.1": ">=3.0.1",
"parse-path@<5.0.0": ">=5.0.0",

Просмотреть файл

@ -39,13 +39,11 @@ export const CONTENT_TYPE_APP_SETTINGS = 'application/vnd.microsoft.bfemulator.d
export const CONTENT_TYPE_WELCOME_PAGE = 'application/vnd.microsoft.bfemulator.document.welcome';
export const CONTENT_TYPE_TRANSCRIPT = 'application/vnd.microsoft.bfemulator.document.transcript';
export const CONTENT_TYPE_LIVE_CHAT = SharedConstants.ContentTypes.CONTENT_TYPE_LIVE_CHAT;
export const CONTENT_TYPE_NGROK_DEBUGGER = SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER;
export const NAVBAR_BOT_EXPLORER = 'navbar.botExplorer';
export const NAVBAR_SETTINGS = 'navbar.settings';
export const NAVBAR_NOTIFICATIONS = 'navbar.notifications';
export const NAVBAR_RESOURCES = 'navbar.resources';
export const NAVBAR_NGROK_DEBUGGER = 'navbar.ngrokDebugger';
export const EDITOR_KEY_PRIMARY = 'primary';
export const EDITOR_KEY_SECONDARY = 'secondary';

Просмотреть файл

@ -149,7 +149,6 @@ export class BotSagas {
})
);
// call emulator to report proper status to chat panel (listening / ngrok)
res = yield ConversationService.sendInitialLogReport(serverUrl, conversationId, action.payload.endpoint);
if (!res.ok) {
yield* throwErrorFromResponse('Error occurred while sending the initial log report', res);

Просмотреть файл

@ -120,14 +120,12 @@ describe('The frameworkSettingsSagas', () => {
useCustomId: false,
usePrereleases: false,
userGUID: '',
ngrokPath: 'some/path/to/ngrok',
};
const updatedSettings: Partial<FrameworkSettings> = {
autoUpdate: true,
useCustomId: true,
usePrereleases: false,
userGUID: 'some-user-id',
ngrokPath: 'some/different/path/to/ngrok',
};
const gen = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction(updatedSettings));
// selector to get the active document from the state
@ -147,7 +145,6 @@ describe('The frameworkSettingsSagas', () => {
autoUpdate: true,
useCustomId: true,
userGUID: 'some-user-id',
ngrokPath: 'some/different/path/to/ngrok',
}
)
);

Просмотреть файл

@ -73,8 +73,6 @@ import {
Settings,
ThemeState,
UpdateState,
ngrokTunnel,
NgrokTunnelState,
} from '@bfemulator/app-shared';
import { forwardToMain } from './middleware/forwardToMain';
@ -98,7 +96,6 @@ export interface RootState {
settings?: Settings;
theme?: ThemeState;
update?: UpdateState;
ngrokTunnel?: NgrokTunnelState;
}
const DEFAULT_STATE = {};
@ -137,7 +134,6 @@ function initStore(): Store<RootState> {
settings: settingsReducer,
theme,
update,
ngrokTunnel,
}),
DEFAULT_STATE,
storeEnhancer

Просмотреть файл

@ -1,203 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
close as closeEditorDocument,
executeCommand,
framework,
ngrokTunnel,
saveFrameworkSettings,
setFrameworkSettings,
SharedConstants,
} from '@bfemulator/app-shared';
import { mount } from 'enzyme';
import * as React from 'react';
import { combineReducers, createStore } from 'redux';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { frameworkDefault } from '@bfemulator/app-shared';
import { getTabGroupForDocument } from '../../../state/helpers/editorHelpers';
import { ariaAlertService } from '../../a11y';
import { AppSettingsEditor } from './appSettingsEditor';
import { AppSettingsEditorContainer } from './appSettingsEditorContainer';
jest.mock('electron', () => ({
remote: {
app: {
isPackaged: false,
},
},
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('The AppSettingsEditorContainer', () => {
let instance: AppSettingsEditor;
let node;
let mockDispatch;
let mockStore;
let commandService: CommandServiceImpl;
const mockCallsMade = [];
const mockRemoteCallsMade = [];
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.call = (commandName, ...args) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve(true) as any;
};
commandService.remoteCall = (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve('hai!') as any;
};
});
beforeEach(() => {
mockStore = createStore(combineReducers({ framework, ngrokTunnel }));
mockStore.dispatch(
setFrameworkSettings({
autoUpdate: true,
bypassNgrokLocalhost: true,
runNgrokAtStartup: false,
collectUsageData: true,
hash: 'someHash',
locale: '',
localhost: '',
ngrokPath: '',
stateSizeLimit: 64,
use10Tokens: false,
useCodeValidation: false,
usePrereleases: false,
})
);
mockDispatch = jest.spyOn(mockStore, 'dispatch');
const wrapper = mount(<AppSettingsEditorContainer store={mockStore} />);
node = wrapper.find(AppSettingsEditor);
instance = node.instance() as AppSettingsEditor;
});
it('should update the state when the value of a checkbox changes', () => {
(instance as any).onChangeCheckBox({
target: {
checked: true,
name: 'runNgrokAtStartup',
},
} as any);
expect(instance.state.runNgrokAtStartup).toBeTruthy();
});
it('should update the state when an input field is changed', () => {
(instance as any).onInputChange({
target: {
value: 'a new value',
name: 'ngrokPath',
},
} as any);
expect(instance.state.ngrokPath).toBe('a new value');
});
it('should call a remote command to open a browse window when "onClickBrowse" is called', async () => {
const dispatchSpy = jest.spyOn(mockStore, 'dispatch').mockImplementation(action => {
if (action.payload.resolver) {
action.payload.resolver('some/path');
}
});
await (instance as any).onClickBrowse();
expect(dispatchSpy).toHaveBeenLastCalledWith({
payload: { dirty: true, documentId: undefined },
type: 'EDITOR/SET_DIRTY_FLAG',
});
expect(instance.state.ngrokPath).toBe('some/path');
});
it('should discard the changes when "discardChanges" is called', () => {
instance.props.discardChanges();
expect(mockDispatch).toHaveBeenCalledWith(
closeEditorDocument(getTabGroupForDocument('app:settings'), 'app:settings')
);
});
it('should save the framework settings then get them again from main when the "onSaveClick" handler is called', async () => {
const alertServiceSpy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined);
const mockPathToNgrokInputRef = { focus: jest.fn() };
(instance as any).pathToNgrokInputRef = mockPathToNgrokInputRef;
await (instance as any).onSaveClick();
const keys = Object.keys(frameworkDefault).sort();
const normalizedState = keys.reduce((s, key) => ((s[key] = instance.state[key]), s), {}) as FrameworkSettings;
const saveSettingsAction = saveFrameworkSettings(normalizedState);
const savedSettings: any = {
...saveSettingsAction.payload,
hash: expect.any(String),
};
expect(mockDispatch).toHaveBeenLastCalledWith(saveFrameworkSettings(savedSettings));
expect(alertServiceSpy).toHaveBeenCalledWith('App settings saved.');
expect(mockPathToNgrokInputRef.focus).toHaveBeenCalled();
});
it('should call the appropriate command when onAnchorClick is called', () => {
instance.props.onAnchorClick('http://blah');
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Electron.OpenExternal, null, 'http://blah')
);
});
});

Просмотреть файл

@ -44,11 +44,10 @@ import {
} from '@bfemulator/ui-react';
import * as React from 'react';
import { ChangeEvent } from 'react';
import { ipcRenderer } from 'electron';
import { GenericDocument } from '../../layout';
import { generateHash } from '../../../state/helpers/botHelpers';
import { TunnelCheckTimeInterval, TunnelStatus } from '../../../state/actions/ngrokTunnelActions';
import { NgrokStatusIndicator } from '../ngrokDebugger/ngrokStatusIndicator';
import * as styles from './appSettingsEditor.scss';
@ -56,16 +55,12 @@ export interface AppSettingsEditorProps {
documentId?: string;
dirty?: boolean;
framework?: FrameworkSettings;
ngrokTunnelStatus?: TunnelStatus;
ngrokLastPingInterval?: TunnelCheckTimeInterval;
createAriaAlert?: (msg: string) => void;
discardChanges?: () => void;
onAnchorClick?: (url: string) => void;
openBrowseForNgrok: () => Promise<string>;
saveFrameworkSettings?: (framework: FrameworkSettings) => void;
setDirtyFlag?: (dirty: boolean) => void;
onOpenNgrokStatusViewerClick: () => void;
}
export interface AppSettingsEditorState extends Partial<FrameworkSettings> {
@ -79,7 +74,6 @@ function shallowEqual(x: any, y: any) {
export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, AppSettingsEditorState> {
public state = {} as AppSettingsEditorState;
private pathToNgrokInputRef: HTMLInputElement;
public static getDerivedStateFromProps(
newProps: AppSettingsEditorProps,
@ -88,7 +82,6 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
if (newProps.framework.hash === prevState.hash) {
return prevState;
}
return {
...newProps.framework,
dirty: newProps.dirty,
@ -96,25 +89,21 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
};
}
public componentDidMount(): void {
if (this.pathToNgrokInputRef) {
this.pathToNgrokInputRef.focus();
}
public async componentDidMount(): Promise<void> {
this.setState({ localPort: await this.getLocalPort() });
}
public render(): JSX.Element {
const {
ngrokPath = '',
useCustomId = false,
bypassNgrokLocalhost = true,
runNgrokAtStartup = false,
localhost = '',
locale = '',
use10Tokens = false,
useCodeValidation = false,
userGUID = '',
autoUpdate = false,
usePrereleases = false,
collectUsageData = false,
tunnelUrl = '',
localPort = 0,
} = this.state;
const inputProps = {
@ -124,85 +113,6 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
return (
<GenericDocument className={styles.appSettingsEditor}>
<Row>
<Column className={styles.spacing}>
<div>
<span className={styles.legend}>Service</span>
<p>
<LinkButton linkRole={true} onClick={this.onNgrokDocsClick}>
ngrok
</LinkButton>{' '}
is network tunneling software. The Bot Framework Emulator works with ngrok to communicate with bots
hosted remotely. Read the{' '}
<LinkButton linkRole={true} onClick={this.onNgrokTunnelingDocsClick}>
wiki page
</LinkButton>{' '}
to learn more about using ngrok and how to download it.
</p>
<Row align={RowAlignment.Center} className={styles.marginBottomRow}>
<TextField
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
inputRef={this.setNgrokInputRef}
readOnly={false}
value={ngrokPath}
onChange={this.onInputChange}
name="ngrokPath"
label={'Path to ngrok'}
/>
<PrimaryButton onClick={this.onClickBrowse} text="Browse" className={styles.browseButton} />
</Row>
<Checkbox
className={styles.checkboxOverrides}
checked={bypassNgrokLocalhost}
onChange={this.onChangeCheckBox}
id="ngrok-bypass"
aria-label="Bypass ngrok for local addresses, Service"
label="Bypass ngrok for local addresses"
name="bypassNgrokLocalhost"
/>
<Checkbox
className={styles.checkboxOverrides}
checked={runNgrokAtStartup}
onChange={this.onChangeCheckBox}
id="ngrok-startup"
aria-label="Run ngrok when the Emulator starts up, Service"
label="Run ngrok when the Emulator starts up"
name="runNgrokAtStartup"
/>
<Row align={RowAlignment.Center} className={styles.marginBottomRow}>
<TextField
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={localhost}
onChange={this.onInputChange}
name="localhost"
label="localhost override"
/>
</Row>
<Row align={RowAlignment.Center}>
<TextField
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={locale}
name="locale"
onChange={this.onInputChange}
label="Locale"
/>
</Row>
<div className={styles.tunnelStatus}>
<NgrokStatusIndicator
tunnelStatus={this.props.ngrokTunnelStatus}
timeIntervalSinceLastPing={this.props.ngrokLastPingInterval}
header="Tunnel Status"
/>
<LinkButton linkRole={true} onClick={this.props.onOpenNgrokStatusViewerClick}>
Click here to go to the Ngrok Status viewer
</LinkButton>
</div>
</div>
</Column>
<Column className={[styles.rightColumn, styles.spacing].join(' ')}>
<div>
<span className={styles.legend}>User settings</span>
@ -270,6 +180,22 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
name="usePrereleases"
/>
</div>
<div>
<span className={styles.legend}>Configure Tunnel</span>
<span>Configure a tunnel to port {localPort}: </span>
<b contentEditable="true">devtunnel host -a -p {localPort}</b>
<Row className={styles.marginBottomRow} align={RowAlignment.Top}>
<TextField
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={tunnelUrl}
name="tunnelUrl"
onChange={this.onInputChange}
label="Tunnel Url"
/>
</Row>
</div>
<div>
<span className={styles.legend}>Data Collection</span>
<Checkbox
@ -315,16 +241,6 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
this.updateDirtyFlag(change);
};
private onClickBrowse = async (): Promise<void> => {
const ngrokPath = await this.props.openBrowseForNgrok();
if (ngrokPath === null) {
return; // Cancelled browse dialog
}
const change = { ngrokPath };
this.setState(change);
this.updateDirtyFlag(change);
};
private onInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const { value, name } = event.target;
const change = { [name]: value };
@ -332,12 +248,6 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
this.updateDirtyFlag(change);
};
private onNgrokDocsClick = this.createAnchorClickHandler('https://ngrok.com/');
private onNgrokTunnelingDocsClick = this.createAnchorClickHandler(
'https://github.com/Microsoft/BotFramework-Emulator/wiki/Tunneling-(ngrok)'
);
private onPrivacyStatementClick = this.createAnchorClickHandler('https://privacy.microsoft.com/privacystatement');
private onSaveClick = async () => {
@ -350,9 +260,6 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
this.setState({ dirty: false });
this.props.saveFrameworkSettings(newState);
this.props.createAriaAlert('App settings saved.');
if (this.pathToNgrokInputRef) {
this.pathToNgrokInputRef.focus();
}
};
private updateDirtyFlag(change: { [prop: string]: any }) {
@ -361,7 +268,8 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
this.props.setDirtyFlag(dirty);
}
private setNgrokInputRef = (ref: HTMLInputElement): void => {
this.pathToNgrokInputRef = ref;
private getLocalPort = async () => {
const lp = await ipcRenderer.invoke('local-server-port');
return lp;
};
}

Просмотреть файл

@ -53,8 +53,6 @@ import { AppSettingsEditor, AppSettingsEditorProps } from './appSettingsEditor';
const mapStateToProps = (state: RootState, ownProps: AppSettingsEditorProps) => ({
...ownProps,
framework: state.framework,
ngrokLastPingInterval: state.ngrokTunnel.timeIntervalSinceLastPing,
ngrokTunnelStatus: state.ngrokTunnel.tunnelStatus,
});
const mapDispatchToProps = (dispatch: (action: Action) => void, ownProps: AppSettingsEditorProps) => ({
@ -66,18 +64,6 @@ const mapDispatchToProps = (dispatch: (action: Action) => void, ownProps: AppSet
onAnchorClick: (url: string) => {
dispatch(executeCommand(true, SharedConstants.Commands.Electron.OpenExternal, null, url));
},
openBrowseForNgrok: async () => {
const dialogOptions = {
title: 'Browse for ngrok',
buttonLabel: 'Select ngrok',
properties: ['openFile'],
};
return new Promise(resolve => {
dispatch(executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, resolve, dialogOptions));
});
},
onOpenNgrokStatusViewerClick: () =>
dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.OpenStatusViewer, null)),
saveFrameworkSettings: (framework: FrameworkSettings) => dispatch(saveFrameworkSettings(framework)),
setDirtyFlag: debounce((dirty: boolean) => dispatch(setDirtyFlag(ownProps.documentId, dirty)), 300),
});

Просмотреть файл

@ -39,7 +39,7 @@ import { Document } from '../../state/reducers/editor';
import { MarkdownPage } from './markdownPage/markdownPage';
import { AppSettingsEditorContainer, EmulatorContainer, WelcomePageContainer, NgrokDebuggerContainer } from './index';
import { AppSettingsEditorContainer, EmulatorContainer, WelcomePageContainer } from './index';
interface EditorFactoryProps {
document?: Document;
@ -77,9 +77,6 @@ export class EditorFactory extends React.Component<EditorFactoryProps> {
/>
);
case SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER:
return <NgrokDebuggerContainer documentId={document.documentId} dirty={this.props.document.dirty} />;
default:
return false;
}

Просмотреть файл

@ -316,13 +316,6 @@ describe('logEntry component', () => {
expect(networkResItem).not.toBeNull();
});
it('should render an ngrok expiration item', () => {
wrapper = mount(<LogEntry {...props} />);
instance = wrapper.instance();
const ngrokitem = instance.renderItem({ type: 'ngrok-expiration', payload: { text: 'some text' } }, 'someKey');
expect(ngrokitem).not.toBeNull();
});
it('should render a luis editor deep link item', () => {
wrapper = mount(<LogEntry {...props} />);
instance = wrapper.instance();

Просмотреть файл

@ -42,7 +42,6 @@ import {
LuisEditorDeepLinkLogItem,
NetworkRequestLogItem,
NetworkResponseLogItem,
NgrokExpirationLogItem,
OpenAppSettingsLogItem,
TextLogItem,
} from '@bfemulator/sdk-shared';
@ -60,7 +59,6 @@ export interface LogEntryProps {
launchLuisEditor?: () => void;
setInspectorObjects?: (documentId: string, objs: any) => void;
setHighlightedObjects?: (documentId: string, objs: any) => void;
reconnectNgrok?: () => void;
showAppSettings?: () => void;
trackEvent?: (name: string, properties?: { [key: string]: any }) => void;
}
@ -169,11 +167,6 @@ export class LogEntry extends React.Component<LogEntryProps> {
return this.renderNetworkResponseItem(body, headers, statusCode, statusMessage, srcUrl, key);
}
case LogItemType.NgrokExpiration: {
const { text } = item.payload as NgrokExpirationLogItem;
return this.renderNgrokExpirationItem(text, key);
}
default:
return false;
}
@ -328,21 +321,6 @@ export class LogEntry extends React.Component<LogEntryProps> {
}
}
renderNgrokExpirationItem(text: string, key: string): JSX.Element {
return (
<span key={key} className={`${styles.spaced} ${styles.level3}`}>
{text + ' '}
<button
aria-label={`Please recoonect, Ngrok connection expired. Log entry ${this.props.entryIndex}`}
className={styles.link}
onClick={() => this.props.reconnectNgrok()}
>
Please reconnect.
</button>
</span>
);
}
renderLuisEditorDeepLinkItem(text: string, key: string): JSX.Element {
return (
<span key={key} className={`text-item ${styles.spaced} ${styles.level3}`}>

Просмотреть файл

@ -68,9 +68,6 @@ function mapDispatchToProps(dispatch: any): Partial<LogEntryProps> {
},
setInspectorObjects: (documentId: string, obj: any) => dispatch(setInspectorObjects(documentId, obj)),
setHighlightedObjects: (documentId: string, obj: any) => dispatch(setHighlightedObjects(documentId, obj)),
reconnectNgrok: () => {
dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.Reconnect));
},
showAppSettings: () => {
const { UI } = SharedConstants.Commands;
return dispatch(executeCommand(false, UI.ShowAppSettings));

Просмотреть файл

@ -36,4 +36,3 @@ export * from './editor';
export * from './panel/panel';
export * from './emulator';
export * from './welcomePage';
export * from './ngrokDebugger/ngrokDebuggerContainer';

Просмотреть файл

@ -1,179 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import React, { useEffect, useRef } from 'react';
import { Column, Row, LinkButton, SmallHeader } from '@bfemulator/ui-react';
import { TunnelCheckTimeInterval, TunnelError, TunnelStatus } from '@bfemulator/app-shared';
import { GenericDocument } from '../../layout';
import { NgrokErrorHandler } from './ngrokErrorHandler';
import * as styles from './ngrokDebuggerContainer.scss';
import { NgrokStatusIndicator } from './ngrokStatusIndicator';
export interface NgrokDebuggerProps {
inspectUrl: string;
errors: TunnelError;
publicUrl: string;
logPath: string;
postmanCollectionPath: string;
tunnelStatus: TunnelStatus;
lastPingedTimestamp: number;
timeIntervalSinceLastPing: TunnelCheckTimeInterval;
onAnchorClick: (linkRef: string) => void;
onSaveFileClick: (originalFilePath: string, dialogOptions: Electron.SaveDialogOptions) => void;
onPingTunnelClick: () => void;
onReconnectToNgrokClick: () => void;
}
const getDialogOptions = (title: string, buttonLabel = 'Save'): Electron.SaveDialogOptions => ({
title,
buttonLabel,
});
export const NgrokDebugger = (props: NgrokDebuggerProps) => {
const convertToAnchorOnClick = (link: string) => {
props.onAnchorClick(link);
};
const errorDetailsContainer =
props.tunnelStatus === TunnelStatus.Error ? (
<div className={styles.errorDetailedViewer}>
<NgrokErrorHandler
errors={props.errors}
onExternalLinkClick={convertToAnchorOnClick}
onReconnectToNgrokClick={props.onReconnectToNgrokClick}
/>
</div>
) : null;
const tunnelConnections = (
<section>
<SmallHeader>Tunnel Connections</SmallHeader>
<ul className={styles.tunnelDetailsList}>
<li>
<legend>Inspect Url</legend>
<LinkButton
ariaLabel="Ngrok Inspect Url."
linkRole={true}
onClick={() => convertToAnchorOnClick(props.inspectUrl)}
>
{props.inspectUrl}
</LinkButton>
</li>
<li>
<legend>Public Url</legend>
<LinkButton
ariaLabel="Ngrok Public Url."
linkRole={true}
onClick={() => convertToAnchorOnClick(props.publicUrl)}
>
{props.publicUrl}
</LinkButton>
</li>
<li>
<LinkButton
ariaLabel="Download Log file."
linkRole={false}
onClick={() => props.onSaveFileClick(props.logPath, getDialogOptions('Save log file to disk.'))}
>
Click here
</LinkButton>
&nbsp;to download the ngrok log file for this tunnel session
</li>
<li>
<LinkButton
ariaLabel="Download postman collection."
linkRole={false}
onClick={() =>
props.onSaveFileClick(props.postmanCollectionPath, getDialogOptions('Save Postman collection to disk.'))
}
>
Click here
</LinkButton>
&nbsp;to download a Postman collection to additionally inspect your tunnels
</li>
</ul>
</section>
);
let pingTunnelInputRef: HTMLButtonElement;
const setPingTunnelInputRef = (ref: HTMLButtonElement): void => {
pingTunnelInputRef = ref;
};
useEffect(() => {
if (pingTunnelInputRef) {
pingTunnelInputRef.focus();
}
});
return (
<GenericDocument className={styles.ngrokDebuggerContainer}>
<h1>
<b>Ngrok Status Viewer</b>
</h1>
<Row>
<Column>
<section>
<SmallHeader>Tunnel Health</SmallHeader>
<div className={styles.tunnelDetailsList}>
<div>
<span>
<NgrokStatusIndicator
tunnelStatus={props.tunnelStatus}
timeIntervalSinceLastPing={props.timeIntervalSinceLastPing}
header="Tunnel Status"
/>
</span>
</div>
<div>
<LinkButton
linkRole={true}
ariaLabel={'Click here to ping the tunnel now'}
onClick={props.onPingTunnelClick}
buttonRef={setPingTunnelInputRef}
>
Click here
</LinkButton>
&nbsp;to ping the tunnel now
</div>
{errorDetailsContainer}
</div>
</section>
{props.publicUrl ? tunnelConnections : null}
</Column>
</Row>
</GenericDocument>
);
};

Просмотреть файл

@ -1,58 +0,0 @@
.ngrok-debugger-container {
:global {
input {
font-family: var(--default-font-family);
}
align-items: flex-center;
justify-content: flex-center;
}
h2 {
font-weight: bold;
}
.tunnel-details-list {
background-color: var(--my-bots-well-bg);
border: var(--my-bots-well-border);
padding: 10px;
legend {
display: block;
font-weight: bold;
margin-bottom: 2px;
}
li {
margin-bottom: 10px;
}
}
.error-detailed-viewer {
border: 1px solid var(--focused-outline);
color: var(--error-text);
}
.console-viewer {
background: var(--success-bg);
}
.error-window {
margin: 0;
color: var(--error-outline);
}
.spacing {
padding: 0 0 2px 3px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
&.well {
padding: 10px;
}
}
}

Просмотреть файл

@ -1,24 +0,0 @@
// This is a generated file. Changes are likely to result in being overwritten
declare namespace NgrokDebuggerContainerScssNamespace {
export interface INgrokDebuggerContainerScss {
'console-viewer': string;
consoleViewer: string;
'error-detailed-viewer': string;
'error-window': string;
errorDetailedViewer: string;
errorWindow: string;
'ngrok-debugger-container': string;
ngrokDebuggerContainer: string;
spacing: string;
'tunnel-details-list': string;
tunnelDetailsList: string;
well: string;
}
}
declare const NgrokDebuggerContainerScssModule: NgrokDebuggerContainerScssNamespace.INgrokDebuggerContainerScss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: NgrokDebuggerContainerScssNamespace.INgrokDebuggerContainerScss;
};
export = NgrokDebuggerContainerScssModule;

Просмотреть файл

@ -1,229 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//TODO: More UI tests to be added
import * as React from 'react';
import { Provider } from 'react-redux';
import { combineReducers, createStore } from 'redux';
import { mount, ReactWrapper } from 'enzyme';
import {
ngrokTunnel,
updateNewTunnelInfo,
TunnelInfo,
TunnelStatus,
updateTunnelStatus,
updateTunnelError,
} from '@bfemulator/app-shared';
import { NgrokDebuggerContainer } from './ngrokDebuggerContainer';
import { NgrokDebugger } from './ngrokDebugger';
const mockClasses = jest.fn(() => ({
errorDetailedViewer: 'error-window',
tunnelActive: 'tunnel-active',
tunnelInactive: 'tunnel-inactive',
tunnelError: 'tunnel-error',
}));
jest.mock('./ngrokDebuggerContainer.scss', () => ({
get errorDetailedViewer() {
return mockClasses().errorDetailedViewer;
},
get tunnelActive() {
return mockClasses().tunnelActive;
},
get tunnelError() {
return mockClasses().tunnelError;
},
get tunnelInactive() {
return mockClasses().tunnelInactive;
},
}));
jest.mock('electron', () => ({
remote: {
app: {
isPackaged: false,
},
},
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('Ngrok Debugger container', () => {
let parent: ReactWrapper;
let wrapper: ReactWrapper;
const mockStore = createStore(combineReducers({ ngrokTunnel }));
let mockDispatch;
const mockClassesImpl = mockClasses();
beforeAll(() => {
parent = mount(
<Provider store={mockStore}>
<NgrokDebuggerContainer />
</Provider>
);
wrapper = parent.find(NgrokDebugger);
});
beforeEach(() => {
const info: TunnelInfo = {
publicUrl: 'https://ncfdsd.ngrok.io/',
inspectUrl: 'http://127.0.0.1:4000',
logPath: 'ngrok.log',
postmanCollectionPath: 'postman.json',
};
mockDispatch = jest.spyOn(mockStore, 'dispatch');
mockStore.dispatch(updateNewTunnelInfo(info));
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Inactive,
})
);
});
it('should render without errors', () => {
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Active,
})
);
expect(wrapper.find(NgrokDebugger)).toBeDefined();
});
it('should show the new tunnel status when tunnel status is changed from an action', () => {
expect(wrapper.find(mockClassesImpl.errorDetailedViewer)).toEqual({});
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
expect(wrapper.find(mockClassesImpl.errorDetailedViewer)).toBeDefined();
});
it('should update classes when tunnel status changes', () => {
expect(wrapper.find(mockClassesImpl.tunnelInactive)).toBeDefined();
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Active,
})
);
expect(wrapper.find(mockClassesImpl.tunnelActive)).toBeDefined();
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
expect(wrapper.find(mockClassesImpl.tunnelError)).toBeDefined();
});
it('should show that tunnel has expired', () => {
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
mockStore.dispatch(
updateTunnelError({
statusCode: 402,
errorMessage: 'Tunnel has expired',
})
);
expect(wrapper.text().includes('ngrok tunnel has expired')).toBeTruthy();
});
it('should show that tunnel has too many connections', () => {
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
mockStore.dispatch(
updateTunnelError({
statusCode: 429,
errorMessage: 'Tunnel has too many connections',
})
);
expect(wrapper.html().includes('Signup for Ngrok account')).toBeTruthy();
});
it('should show that tunnel has expired', () => {
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
mockStore.dispatch(
updateTunnelError({
statusCode: 402,
errorMessage: 'Tunnel has expired',
})
);
expect(wrapper.html().includes('ngrok tunnel has expired.')).toBeTruthy();
});
it('should show a generic tunnel error message', () => {
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
mockStore.dispatch(
updateTunnelError({
statusCode: -9999,
errorMessage: 'Dummy tunnel error',
})
);
expect(wrapper.html().includes('Looks like the ngrok tunnel does not exist anymore.')).toBeTruthy();
});
});

Просмотреть файл

@ -1,102 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { connect } from 'react-redux';
import { Action } from 'redux';
import { executeCommand, SharedConstants } from '@bfemulator/app-shared';
import { RootState } from '../../../state/store';
import { NgrokDebugger, NgrokDebuggerProps } from './ngrokDebugger';
const onFileSaveCb = (result: boolean) => {
if (!result) {
// TODO: Show error dialog here
// eslint-disable-next-line no-console
console.error('An error occured trying to save the file to disk');
}
};
const mapStateToProps = (state: RootState, ownProps: Record<string, unknown>): Partial<NgrokDebuggerProps> => {
const {
inspectUrl,
errors,
publicUrl,
logPath,
postmanCollectionPath,
tunnelStatus,
lastPingedTimestamp: lastTunnelStatusCheckTS,
timeIntervalSinceLastPing: timeIntervalSinceLastPing,
} = state.ngrokTunnel;
return {
inspectUrl,
errors,
publicUrl,
logPath,
postmanCollectionPath,
tunnelStatus,
lastPingedTimestamp: lastTunnelStatusCheckTS,
timeIntervalSinceLastPing,
...ownProps,
};
};
const mapDispatchToProps = (dispatch: (action: Action) => void) => ({
onAnchorClick: (url: string) =>
dispatch(executeCommand(true, SharedConstants.Commands.Electron.OpenExternal, null, url)),
onPingTunnelClick: () => dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.PingTunnel, null)),
onReconnectToNgrokClick: () => dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.Reconnect, null)),
onSaveFileClick: (originalFilePath: string, dialogOptions: Electron.SaveDialogOptions) => {
dispatch(
executeCommand(
true,
SharedConstants.Commands.Electron.ShowSaveDialog,
(newFilePath: string) => {
dispatch(
executeCommand(
true,
SharedConstants.Commands.Electron.CopyFile,
onFileSaveCb,
originalFilePath,
newFilePath
)
);
},
dialogOptions
)
);
},
});
export const NgrokDebuggerContainer = connect(mapStateToProps, mapDispatchToProps)(NgrokDebugger);

Просмотреть файл

@ -1,116 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import React from 'react';
import { LinkButton } from '@bfemulator/ui-react';
import { TunnelError } from 'packages/app/main/src/state';
export interface NgrokErrorHandlerProps {
errors: TunnelError;
onExternalLinkClick: (linkRef: string) => void;
onReconnectToNgrokClick: () => void;
}
export const NgrokErrorHandler = (props: NgrokErrorHandlerProps) => {
switch (props.errors.statusCode) {
case 429:
return (
<>
<legend role="alert">
Looks like you have hit your free tier limit on connections to your tunnel. Below you will find several
possible solutions.
</legend>
<ol>
<li>
<span>
Signup for an Ngrok Account&nbsp;
<LinkButton
ariaLabel="Signup for Ngrok account"
linkRole={true}
onClick={() => props.onExternalLinkClick('https://dashboard.ngrok.com/signup')}
>
https://dashboard.ngrok.com/signup
</LinkButton>
and register your auth token as per the steps in
<LinkButton
ariaLabel="Github link for tunnelling issues."
linkRole={true}
onClick={() =>
props.onExternalLinkClick('https://github.com/microsoft/botframework-solutions/issues/2406')
}
>
https://github.com/microsoft/botframework-solutions/issues/2406
</LinkButton>
</span>
</li>
<li>Upgrade to a paid account of ngrok</li>
<li>Wait for a few minutes without any activity</li>
</ol>
</>
);
case 402:
return (
<>
<legend role="alert">
Looks like the ngrok tunnel has expired. Try reconnecting to Ngrok or examine the logs for a detailed
explanation of the error.
</legend>
<LinkButton
ariaLabel="Click here to reconnect to ngrok."
linkRole={false}
onClick={props.onReconnectToNgrokClick}
>
Click here to reconnect to ngrok
</LinkButton>
</>
);
default:
return (
<>
<legend role="alert">
Looks like the ngrok tunnel does not exist anymore. Try reconnecting to Ngrok or examine the logs for a
detailed explanation of the error.
</legend>
<LinkButton
ariaLabel="Click here to reconnect to ngrok."
linkRole={false}
onClick={props.onReconnectToNgrokClick}
>
Click here to reconnect to ngrok
</LinkButton>
</>
);
}
};

Просмотреть файл

@ -1,77 +0,0 @@
.ngrok-status-indicator {
:global {
input {
font-family: var(--default-font-family);
}
align-items: flex-center;
justify-content: flex-center;
margin-bottom: 4px;
}
.announcement {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.header {
font-weight: bold;
}
%common-tunnel-indicator {
margin-right: 3px;
padding: 5px;
color: var(--ngrok-text-color);
}
.tunnel-details-list {
background-color: var(--my-bots-well-bg);
border: var(--my-bots-well-border);
padding: 10px;
legend {
display: block;
font-weight: bold;
margin-bottom: 2px;
}
li {
margin-bottom: 10px;
}
}
.tunnel-health-indicator {
display: inline-block;
position: relative;
padding: 3px;
margin-right: 2px;
&.tunnel-error {
&::before {
@extend %common-tunnel-indicator;
background-color: var(--ngrok-error);
content: "Error";
}
}
&.tunnel-inactive {
&::before {
@extend %common-tunnel-indicator;
background-color: var(--ngrok-inactive);
content: "Inactive";
}
}
&.tunnel-active {
&::before {
@extend %common-tunnel-indicator;
background-color: var(--ngrok-active);
content: "Active";
}
}
}
}

Просмотреть файл

@ -1,26 +0,0 @@
// This is a generated file. Changes are likely to result in being overwritten
declare namespace NgrokStatusIndicatorScssNamespace {
export interface INgrokStatusIndicatorScss {
announcement: string;
header: string;
'ngrok-status-indicator': string;
ngrokStatusIndicator: string;
'tunnel-active': string;
'tunnel-details-list': string;
'tunnel-error': string;
'tunnel-health-indicator': string;
'tunnel-inactive': string;
tunnelActive: string;
tunnelDetailsList: string;
tunnelError: string;
tunnelHealthIndicator: string;
tunnelInactive: string;
}
}
declare const NgrokStatusIndicatorScssModule: NgrokStatusIndicatorScssNamespace.INgrokStatusIndicatorScss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: NgrokStatusIndicatorScssNamespace.INgrokStatusIndicatorScss;
};
export = NgrokStatusIndicatorScssModule;

Просмотреть файл

@ -1,71 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//TODO: More UI tests to be added
import * as React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { TunnelStatus, TunnelCheckTimeInterval } from '@bfemulator/app-shared';
import { NgrokStatusIndicator } from './ngrokStatusIndicator';
describe('Ngrok Debugger container', () => {
let parent: ReactWrapper;
let wrapper: ReactWrapper;
beforeAll(() => {
parent = mount(
<NgrokStatusIndicator
timeIntervalSinceLastPing={TunnelCheckTimeInterval.Now}
tunnelStatus={TunnelStatus.Active}
header="Ngrok Tunnel Status"
/>
);
wrapper = parent.find(NgrokStatusIndicator);
});
it('should render without errors', () => {
expect(wrapper.find(NgrokStatusIndicator)).toBeDefined();
});
it('should show correct time interval when updated', () => {
expect(wrapper.find(NgrokStatusIndicator)).toBeDefined();
expect(wrapper.html().includes('Refreshed now')).toBeTruthy();
parent.setProps({ timeIntervalSinceLastPing: TunnelCheckTimeInterval.FirstInterval });
expect(wrapper.html().includes('Refreshed 20 seconds ago')).toBeTruthy();
parent.setProps({ timeIntervalSinceLastPing: TunnelCheckTimeInterval.SecondInterval });
expect(wrapper.html().includes('Refreshed 40 seconds ago')).toBeTruthy();
parent.setProps({ timeIntervalSinceLastPing: TunnelCheckTimeInterval.Now });
expect(wrapper.html().includes('Refreshed now')).toBeTruthy();
});
});

Просмотреть файл

@ -1,100 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import React, { useEffect, useState } from 'react';
import { TunnelCheckTimeInterval, TunnelStatus } from '@bfemulator/app-shared';
import * as styles from './ngrokStatusIndicator.scss';
export interface NgrokTimeIntervalIndicatorProps {
timeIntervalSinceLastPing: TunnelCheckTimeInterval;
tunnelStatus: TunnelStatus;
header: string;
}
export const NgrokStatusIndicator = (props: NgrokTimeIntervalIndicatorProps) => {
const [statusDisplay, setStatusDisplay] = useState(styles.tunnelInactive);
const [displayTimeInterval, setTimeIntervalDisplay] = useState('Last refreshed now');
const [statusAnnouncement, setStatusAnnouncement] = useState('');
useEffect(() => {
switch (props.tunnelStatus) {
case TunnelStatus.Active:
setStatusDisplay(styles.tunnelActive);
setStatusAnnouncement('active');
break;
case TunnelStatus.Error:
setStatusDisplay(styles.tunnelError);
setStatusAnnouncement('error');
break;
default:
setStatusDisplay(styles.tunnelInactive);
setStatusAnnouncement('inactive');
break;
}
}, [props.tunnelStatus]);
useEffect(() => {
if (props.tunnelStatus === TunnelStatus.Inactive) {
setTimeIntervalDisplay('');
return;
}
switch (props.timeIntervalSinceLastPing) {
case TunnelCheckTimeInterval.FirstInterval:
setTimeIntervalDisplay('Refreshed 20 seconds ago...');
break;
case TunnelCheckTimeInterval.SecondInterval:
setTimeIntervalDisplay('Refreshed 40 seconds ago...');
break;
default:
setTimeIntervalDisplay('Refreshed now...');
break;
}
}, [props.timeIntervalSinceLastPing]);
return (
<div className={styles.ngrokStatusIndicator}>
<span className={styles.announcement} role="status">
{props.header} {statusAnnouncement} {displayTimeInterval}
</span>
<span className={styles.header}>{props.header}:</span>
<span className={[styles.tunnelHealthIndicator, statusDisplay].join(' ')}>
<span>{displayTimeInterval}</span>
</span>
</div>
);
};

Просмотреть файл

@ -88,7 +88,7 @@ export class AppMenu extends React.Component<AppMenuProps, Record<string, unknow
private updateMenu(template: { [key: string]: MenuItem[] }): { [key: string]: MenuItem[] } {
const fileMenu = template['file'];
fileMenu[14].items = this.getThemeMenuItems();
fileMenu[12].items = this.getThemeMenuItems();
fileMenu[3].items = this.getRecentBotsMenuItems();
// disable / enable "Close tab" button
fileMenu[9].disabled = !this.props.activeBot;

Просмотреть файл

@ -48,7 +48,7 @@ describe('App menu template', () => {
expect(Object.keys(template)).toEqual(['file', 'debug', 'edit', 'view', 'conversation', 'help']);
expect(template['file']).toHaveLength(19);
expect(template['file']).toHaveLength(17);
expect(template['debug']).toHaveLength(1);
expect(template['edit']).toHaveLength(7);
expect(template['view']).toHaveLength(6);

Просмотреть файл

@ -43,7 +43,6 @@ const {
Electron: { OpenExternal, ShowMessageBox },
Emulator: {
ClearState,
GetServiceUrl,
PromptToOpenTranscript,
SendConversationUpdateUserAdded,
SendBotContactAdded,
@ -51,8 +50,8 @@ const {
SendTyping,
SendPing,
SendDeleteUserData,
GetServerPort,
},
Ngrok: { OpenStatusViewer },
UI: {
ShowBotCreationDialog,
ShowCustomActivityEditor,
@ -102,13 +101,6 @@ export class AppMenuTemplate {
{ type: 'separator' },
{ label: 'Open Transcript...', onClick: () => AppMenuTemplate.commandService.call(PromptToOpenTranscript) },
{ type: 'separator' },
{
label: 'Open Ngrok Status Viewer...',
onClick: () => {
AppMenuTemplate.commandService.remoteCall(OpenStatusViewer);
},
},
{ type: 'separator' },
{ label: 'Close tab', disabled: true, onClick: () => AppMenuTemplate.commandService.call(Close) },
{ type: 'separator' },
{ label: 'Sign in with Azure' }, // onClick defined later
@ -121,10 +113,10 @@ export class AppMenuTemplate {
},
{ type: 'separator' },
{
label: 'Copy Emulator service URL',
label: 'Copy Emulator service Port',
onClick: async () => {
const url: string = await AppMenuTemplate.commandService.remoteCall(GetServiceUrl);
remote.clipboard.writeText(url);
const port: number = await AppMenuTemplate.commandService.remoteCall(GetServerPort);
remote.clipboard.writeText(port.toString());
},
},
{ type: 'separator' },

Просмотреть файл

@ -1,49 +0,0 @@
.ngrok-tab {
display: flex;
position: relative;
width: 100%;
height: 100%;
@mixin statusIndicator($color) {
.status-indicator {
background-color: var($color);
}
}
@keyframes blinker {
50% {
background-color: transparent;
}
}
&.blinker-error {
&.tunnel-error {
.status-indicator {
animation: blinker 1s linear infinite;
}
}
}
&.tunnel-error {
@include statusIndicator(--ngrok-error)
}
&.tunnel-inactive {
@include statusIndicator(--ngrok-inactive)
}
&.tunnel-active {
@include statusIndicator(--ngrok-active)
}
.status-indicator{
z-index: 1;
width: 5px;
height: 5px;
border-radius: 50%;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
}

Просмотреть файл

@ -1,25 +0,0 @@
// This is a generated file. Changes are likely to result in being overwritten
declare namespace NgrokTabScssNamespace {
export interface INgrokTabScss {
blinker: string;
'blinker-error': string;
blinkerError: string;
'ngrok-tab': string;
ngrokTab: string;
'status-indicator': string;
statusIndicator: string;
'tunnel-active': string;
'tunnel-error': string;
'tunnel-inactive': string;
tunnelActive: string;
tunnelError: string;
tunnelInactive: string;
}
}
declare const NgrokTabScssModule: NgrokTabScssNamespace.INgrokTabScss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: NgrokTabScssNamespace.INgrokTabScss;
};
export = NgrokTabScssModule;

Просмотреть файл

@ -1,173 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import * as React from 'react';
import { mount } from 'enzyme';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import {
editor,
ngrokTunnel,
updateNewTunnelInfo,
updateTunnelStatus,
TunnelInfo,
TunnelStatus,
} from '@bfemulator/app-shared';
import { NgrokTabContainer } from './ngrokTabContainer';
import { NgrokTab } from './ngrokTab';
jest.mock('./ngrokTab.scss', () => ({
ngrokTab: 'ngrok',
tunnelActive: 'active',
tunnelError: 'error',
tunnelInactive: 'inactive',
blinkerError: 'blinker',
}));
jest.mock('electron', () => ({
remote: {
app: {
isPackaged: false,
},
},
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
let mockStore;
jest.mock('../../../../../src/state/store', () => ({
get store() {
return mockStore;
},
}));
let mockDispatch;
let wrapper;
let node;
let instance;
describe('Ngrok Tab', () => {
const mockOnCloseClick = jest.fn(() => null);
beforeEach(() => {
mockStore = createStore(combineReducers({ ngrokTunnel, editor }));
mockDispatch = jest.spyOn(mockStore, 'dispatch');
wrapper = mount(
<Provider store={mockStore}>
<NgrokTabContainer
active={false}
dirty={false}
documentId="doc-1"
label="Ngrok"
onCloseClick={mockOnCloseClick}
/>
</Provider>
);
const info: TunnelInfo = {
publicUrl: 'https://ncfdsd.ngrok.io/',
inspectUrl: 'http://127.0.0.1:4000',
logPath: 'ngrok.log',
postmanCollectionPath: 'postman.json',
};
mockDispatch = jest.spyOn(mockStore, 'dispatch');
mockStore.dispatch(updateNewTunnelInfo(info));
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Active,
})
);
node = wrapper.find(NgrokTab);
instance = node.instance();
});
it('should render without error', () => {
expect(wrapper.find(NgrokTab)).not.toBe(null);
});
it('should remove blinker class when made active', () => {
const unchangedProps = {
dirty: false,
documentId: 'doc-1',
label: 'Ngrok',
onCloseClick: mockOnCloseClick,
tunnelStatus: TunnelStatus.Error,
};
const props = {
...unchangedProps,
active: false,
};
wrapper = mount(<NgrokTab {...props} />);
expect(wrapper.find('.blinker').length).toBe(1);
wrapper.setProps({
active: true,
});
wrapper.update();
expect(wrapper.find('.blinker').length).toBe(0);
wrapper.setProps({
active: false,
});
expect(wrapper.find('.blinker').length).toBe(0);
});
it('should switch to active class when tunnel status changes', done => {
expect(wrapper.html().includes('active')).toBeTruthy();
mockStore.dispatch(
updateTunnelStatus({
tunnelStatus: TunnelStatus.Error,
})
);
wrapper.update();
setTimeout(() => {
expect(wrapper.html().includes('error')).toBeTruthy();
done();
});
});
});

Просмотреть файл

@ -1,99 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import React, { useEffect, useState } from 'react';
import { TunnelStatus } from '@bfemulator/app-shared';
import * as styles from './ngrokTab.scss';
import { Tab, TabProps } from './tab';
export interface NgrokTabProps extends TabProps {
tunnelStatus: TunnelStatus;
}
export const NgrokTab = (props: NgrokTabProps) => {
const [status, setStatus] = useState({
className: styles.tunnelInactive,
ariaLabel: 'Tunnel Active',
});
const [preventAddingBlinker, preventAddingBlinkerAfterActive] = useState(false);
useEffect(() => {
if (props.active && !preventAddingBlinker) {
preventAddingBlinkerAfterActive(true);
}
}, [props.active]);
useEffect(() => {
switch (props.tunnelStatus) {
case TunnelStatus.Active:
setStatus({
className: styles.tunnelActive,
ariaLabel: 'Tunnel Active',
});
break;
case TunnelStatus.Error:
setStatus({
className: styles.tunnelError,
ariaLabel: 'Tunnel Error',
});
break;
default:
setStatus({
className: styles.tunnelInactive,
ariaLabel: 'Tunnel Inactive',
});
break;
}
}, [props.tunnelStatus]);
const blinkerErrorClass = preventAddingBlinker ? '' : styles.blinkerError;
return (
<div className={[styles.ngrokTab, status.className, blinkerErrorClass].join(' ')}>
<Tab
active={props.active}
dirty={props.dirty}
documentId={props.documentId}
index={props.index}
label={props.label}
onCloseClick={props.onCloseClick}
hideIcon={true}
>
<div className={styles.statusIndicator} aria-label={status.ariaLabel} />
</Tab>
</div>
);
};

Просмотреть файл

@ -1,49 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { connect } from 'react-redux';
import { RootState } from '../../../../state/index';
import { NgrokTab, NgrokTabProps } from './ngrokTab';
const mapStateToProps = (state: RootState, ownProps: Record<string, unknown>): Partial<NgrokTabProps> => {
const { tunnelStatus } = state.ngrokTunnel;
return {
tunnelStatus,
...ownProps,
};
};
export const NgrokTabContainer = connect(mapStateToProps, null)(NgrokTab);

Просмотреть файл

@ -44,10 +44,8 @@ import {
CONTENT_TYPE_MARKDOWN,
CONTENT_TYPE_TRANSCRIPT,
CONTENT_TYPE_WELCOME_PAGE,
CONTENT_TYPE_NGROK_DEBUGGER,
} from '../../../../constants';
import { Tab } from '../tab/tab';
import { NgrokTabContainer } from '../tab/ngrokTabContainer';
import * as styles from './tabBar.scss';
@ -195,11 +193,7 @@ export class TabBar extends React.Component<TabBarProps, TabBarState> {
ref={this.setRef}
role="presentation"
>
{documentId === SharedConstants.DocumentIds.DOCUMENT_ID_NGROK_DEBUGGER ? (
<NgrokTabContainer {...commonProps} />
) : (
<Tab {...commonProps} />
)}
<Tab {...commonProps} />
</div>
);
});
@ -293,9 +287,6 @@ export class TabBar extends React.Component<TabBarProps, TabBarState> {
case CONTENT_TYPE_DEBUG:
return 'Debug';
case CONTENT_TYPE_NGROK_DEBUGGER:
return 'Ngrok Status';
default:
return '';
}

Просмотреть файл

@ -102,15 +102,7 @@
-webkit-mask: url('../../media/ic_settings.svg') no-repeat;
}
}
&:nth-child(5) {
// Ngrok Status Viewer
> div {
-webkit-mask-size: 31px;
-webkit-mask: url('../../media/ic_wrench.svg') no-repeat;
}
}
> div {
position: absolute;
width: 24px;

Просмотреть файл

@ -59,7 +59,6 @@ const selectionMap = [
Constants.NAVBAR_RESOURCES,
Constants.NAVBAR_NOTIFICATIONS,
Constants.NAVBAR_SETTINGS,
Constants.NAVBAR_NGROK_DEBUGGER,
];
export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {

Просмотреть файл

@ -302,13 +302,6 @@ html {
--menu-separator-color: #CCCCCC;
--menu-submenu-top-adjustment: -8px;
/* Ngrok Colors */
--ngrok-inactive: #007ACC;
--ngrok-active: #47B07F;
--ngrok-error: #BE1100;
--ngrok-error-outline: #F5B1B1;
--ngrok-text-color: #fff;
/* Webchat style overrides */
--bubble-text-color: #fff;

Просмотреть файл

@ -299,13 +299,6 @@ html {
--menu-separator-color: #72C3DF;
--menu-submenu-top-adjustment: -10px; /* 2 px lower than light & dark because of 2px menu border in HC mode */
/* Ngrok Colors */
--ngrok-inactive: #007ACC;
--ngrok-active: #47B07F;
--ngrok-error: #BE1100;
--ngrok-error-outline: #F5B1B1;
--ngrok-text-color: #000000;
/* Webchat style overrides */
--bubble-text-color: #000000;

Просмотреть файл

@ -302,13 +302,6 @@ html {
--menu-separator-color: #2B2B2B;
--menu-submenu-top-adjustment: -8px;
/* Ngrok Colors */
--ngrok-inactive: #007ACC;
--ngrok-active: #47B07F;
--ngrok-error: #BE1100;
--ngrok-error-outline: #F5B1B1;
--ngrok-text-color: #fff;
/* Webchat style overrides */
--bubble-text-color: #fff;

Просмотреть файл

@ -41,7 +41,6 @@ describe('getSettingsDelta', () => {
};
const updatedSettings: Partial<FrameworkSettings> = {
autoUpdate: true,
runNgrokAtStartup: true,
use10Tokens: false,
usePrereleases: false,
userGUID: 'some-other-id',
@ -49,7 +48,6 @@ describe('getSettingsDelta', () => {
expect(getSettingsDelta(currentSettings, updatedSettings)).toEqual({
locale: undefined,
runNgrokAtStartup: true,
use10Tokens: false,
usePrereleases: false,
userGUID: 'some-other-id',

Просмотреть файл

@ -1,7 +1,7 @@
{
"name": "@bfemulator/main",
"packagename": "BotFramework-Emulator",
"version": "4.14.1",
"version": "4.15.1",
"private": true,
"description": "Development tool for the Microsoft Bot Framework. Allows developers to test and debug bots on localhost.",
"main": "./app/server/main.js",
@ -16,7 +16,7 @@
"lint:fix": "npm run lint -- --fix",
"pack": "electron-builder --config scripts/config/getElectronBuilderConfig.js --dir",
"start": "concurrently --kill-others --names \"electron,react-app\" --success first \"npm run start:electron:dev\" \"npm run start:react-app\"",
"start:electron": "./node_modules/.bin/electron --inspect=7777 --remote-debugging-port=7778 .",
"start:electron": "electron --inspect=7777 --remote-debugging-port=7778 .",
"start:electron:dev": "cross-env ELECTRON_TARGET_URL=http://localhost:3000/ npm run start:electron",
"start:react-app": "cd ../client && npm start",
"start:watch": "nodemon",
@ -143,7 +143,7 @@
"@electron/remote": "^2.0.11",
"@microsoft/bf-chatdown": "4.7.0",
"applicationinsights": "^1.0.8",
"base64url": "3.0.0",
"base64url": "3.0.1",
"botframework-config": "^4.4.0",
"botframework-schema": "^4.17.0",
"chokidar": "^2.0.2",
@ -170,8 +170,8 @@
"restify-cors-middleware2": "^2.2.1",
"rsa-pem-from-mod-exp": "^0.8.4",
"sanitize-filename": "^1.6.1",
"semver": "^5.5.0",
"semver": "^7.6.3",
"tslib": "^1.9.0",
"ws": "^5.0.0"
"ws": "^8.18.0"
}
}

Просмотреть файл

@ -144,14 +144,7 @@ jest.mock('./appUpdater', () => ({
jest.mock('./emulator', () => ({
Emulator: {
initialize: jest.fn(),
getInstance: () => ({
ngrok: {
broadcastNgrokExpired: () => null,
ngrokEmitter: {
on: () => null,
},
},
}),
getInstance: () => ({}),
},
}));

Просмотреть файл

@ -196,7 +196,7 @@ export class AppMenuBuilder {
/** Returns the template to construct a file menu that reflects updated state */
private static getUpdatedFileMenuContent(recentBotsMenu: Menu = new Menu()): MenuOpts[] {
const { Azure, UI, Bot, Emulator: EmulatorCommands, Ngrok } = SharedConstants.Commands;
const { Azure, UI, Bot, Emulator: EmulatorCommands } = SharedConstants.Commands;
// TODO - localization
const subMenu: MenuOpts[] = [
@ -227,12 +227,6 @@ export class AppMenuBuilder {
},
},
{ type: 'separator' },
{
label: 'Open Ngrok Status Viewer...',
click: () => {
AppMenuBuilder.commandService.call(Ngrok.OpenStatusViewer);
},
},
];
const activeBot = BotHelpers.getActiveBot();
@ -290,10 +284,10 @@ export class AppMenuBuilder {
]);
subMenu.push({ type: 'separator' });
subMenu.push({
label: 'Copy Emulator service URL',
label: 'Copy Emulator Port',
click: async () => {
const url = await Emulator.getInstance().ngrok.getServiceUrl('');
clipboard.writeText(url);
const port = await Emulator.getInstance().server.serverPort;
clipboard.writeText(port.toString());
},
});
subMenu.push({ type: 'separator' });

Просмотреть файл

@ -137,13 +137,7 @@ jest.mock('./utils/sendNotificationToClient', () => ({
jest.mock('./emulator', () => ({
Emulator: {
initialize: jest.fn(),
getInstance: () => ({
ngrok: {
ngrokEmitter: {
on: () => null,
},
},
}),
getInstance: () => ({}),
},
}));

Просмотреть файл

@ -168,9 +168,6 @@ const mockEmulator = {
},
getServiceUrl: () => 'http://localhost:6728',
},
ngrok: {
getServiceUrl: () => 'http://localhost:5678',
},
};
jest.mock('../emulator', () => ({
Emulator: {
@ -256,7 +253,7 @@ const mockConversation = mockEmulator.server.state.conversations.newConversation
id: '0a441b55-d1d6-4015-bbb4-2e7f44fa9f42',
name: 'User',
},
serviceUrl: 'https://a457e760.ngrok.io',
serviceUrl: 'https://a457e760.mytunnel.io',
},
},
{
@ -285,14 +282,14 @@ const mockConversation = mockEmulator.server.state.conversations.newConversation
id: '0a441b55-d1d6-4015-bbb4-2e7f44fa9f42',
name: 'User',
},
serviceUrl: 'https://a457e760.ngrok.io',
serviceUrl: 'https://a457e760.mytunnel.io',
},
},
{
type: 'activity add',
activity: {
type: 'message',
serviceUrl: 'https://a457e760.ngrok.io',
serviceUrl: 'https://a457e760.mytunnel.io',
channelId: 'emulator',
from: {
id: 'http://localhost:3978/api/messages',
@ -318,7 +315,7 @@ const mockConversation = mockEmulator.server.state.conversations.newConversation
type: 'activity add',
activity: {
type: 'message',
serviceUrl: 'https://a457e760.ngrok.io',
serviceUrl: 'https://a457e760.mytunnel.io',
channelId: 'emulator',
from: {
id: 'http://localhost:3978/api/messages',
@ -344,7 +341,7 @@ const mockConversation = mockEmulator.server.state.conversations.newConversation
type: 'activity add',
activity: {
type: 'message',
serviceUrl: 'https://a457e760.ngrok.io',
serviceUrl: 'https://a457e760.mytunnel.io',
channelId: 'emulator',
from: {
id: 'http://localhost:3978/api/messages',
@ -636,9 +633,9 @@ describe('The emulatorCommands', () => {
expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_deleteUserData');
});
it('should get the current ngrok service url', async () => {
it('should get the current service url', async () => {
const url = await registry.getCommand(SharedConstants.Commands.Emulator.GetServiceUrl)();
expect(url).toBe('http://localhost:5678');
expect(url).toBe('http://localhost:6728');
});
});

Просмотреть файл

@ -268,6 +268,11 @@ export class EmulatorCommands {
// Returns the current Emulator's service url
@Command(Commands.GetServiceUrl)
protected async getServiceUrl() {
return Emulator.getInstance().ngrok.getServiceUrl('');
return Emulator.getInstance().server.getServiceUrl('');
}
@Command(Commands.GetServerPort)
protected async getServerPort() {
return Emulator.getInstance().server.serverPort;
}
}

Просмотреть файл

@ -38,7 +38,6 @@ import { ConnectedServiceCommands } from './connectedServiceCommands';
import { ElectronCommands } from './electronCommands';
import { EmulatorCommands } from './emulatorCommands';
import { FileCommands } from './fileCommands';
import { NgrokCommands } from './ngrokCommands';
import { OauthCommands } from './oauthCommands';
import { SettingsCommands } from './settingsCommands';
import { TelemetryCommands } from './telemetryCommands';
@ -51,7 +50,6 @@ export const commands = [
new ElectronCommands(),
new EmulatorCommands(),
new FileCommands(),
new NgrokCommands(),
new OauthCommands(),
new SettingsCommands(),
new TelemetryCommands(),

Просмотреть файл

@ -1,126 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { combineReducers, createStore } from 'redux';
import { open as OpenInEditor, OpenEditorAction } from '../state/actions/editorActions';
import { NgrokCommands } from './ngrokCommands';
let mockStore;
jest.mock('../state/store', () => ({
get store() {
return mockStore;
},
}));
const mockEmulator = {
server: {
state: {
endpoints: {
clear: jest.fn(),
set: jest.fn(),
},
},
},
};
jest.mock('../emulator', () => ({
Emulator: {
getInstance: () => mockEmulator,
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('The Ngrok commands', () => {
const {
Commands: { Ngrok },
} = SharedConstants;
let registry: CommandRegistry;
let commandService: CommandServiceImpl;
beforeAll(() => {
mockStore = createStore(combineReducers({}));
new NgrokCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
registry = commandService.registry;
});
it('should fire editor with the correct makeActiveByDefalt default value', done => {
const makeActiveByDefalt = true;
jest.spyOn(mockStore, 'dispatch').mockImplementationOnce((args: OpenEditorAction) => {
expect(args.payload.meta.makeActiveByDefault).toBe(makeActiveByDefalt);
done();
});
const handler = registry.getCommand(Ngrok.OpenStatusViewer);
handler();
});
it('should fire editor action with the correct makeActiveByDefalt set as false', done => {
const makeActiveByDefalt = false;
jest.spyOn(mockStore, 'dispatch').mockImplementationOnce((args: OpenEditorAction) => {
expect(args.payload.meta.makeActiveByDefault).toBe(makeActiveByDefalt);
done();
});
const handler = registry.getCommand(Ngrok.OpenStatusViewer);
handler(makeActiveByDefalt);
});
});

Просмотреть файл

@ -1,81 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { open as openInEditor, SharedConstants } from '@bfemulator/app-shared';
import { Command } from '@bfemulator/sdk-shared';
import { store } from '../state/store';
import { Emulator } from '../emulator';
import { TelemetryService } from '../telemetry';
const Commands = SharedConstants.Commands.Ngrok;
/** Registers ngrok commands */
export class NgrokCommands {
// Attempts to reconnect to a new ngrok tunnel
@Command(Commands.Reconnect)
protected async reconnectToNgrok(): Promise<any> {
const emulator = Emulator.getInstance();
try {
await emulator.ngrok.recycle();
emulator.ngrok.broadcastNgrokReconnected();
TelemetryService.trackEvent('ngrok_reconnect');
} catch (e) {
throw new Error(`There was an error while trying to reconnect ngrok: ${e}`);
}
}
@Command(Commands.KillProcess)
protected killNgrokProcess() {
Emulator.getInstance().ngrok.kill();
}
@Command(Commands.PingTunnel)
protected pingForStatusOfTunnel() {
Emulator.getInstance().ngrok.pingTunnel();
}
@Command(Commands.OpenStatusViewer)
protected openStatusViewer(makeActiveByDefault = true) {
store.dispatch(
openInEditor({
contentType: SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER,
documentId: SharedConstants.DocumentIds.DOCUMENT_ID_NGROK_DEBUGGER,
isGlobal: true,
meta: {
makeActiveByDefault,
},
})
);
}
}

Просмотреть файл

@ -31,21 +31,16 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { NgrokService } from './ngrokService';
import { defaultRestServerOptions, EmulatorRestServer, EmulatorRestServerOptions } from './server/restServer';
import { getSettings } from './state/store';
let emulator: Emulator;
/**
* Top-level state container for the Node process.
*/
export class Emulator {
public ngrok: NgrokService;
private _server: EmulatorRestServer;
private constructor() {
this.ngrok = new NgrokService();
}
public static initialize(): void {
if (!emulator) {
emulator = new Emulator();
@ -68,14 +63,18 @@ export class Emulator {
return this._server;
}
private async getServiceUrl(s: string) {
if (s) return s;
else return `http://localhost:${Emulator.getInstance().server.serverPort}`;
}
/** Initializes the emulator rest server. No-op if already called. */
public initServer(options: EmulatorRestServerOptions = defaultRestServerOptions): void {
if (!this._server) {
this._server = new EmulatorRestServer({
...options,
getServiceUrl: botUrl => this.ngrok.getServiceUrl(botUrl),
getServiceUrlForOAuth: () => this.ngrok.getServiceUrlForOAuth(),
shutDownOAuthNgrokInstance: () => this.ngrok.shutDownOAuthNgrokInstance(),
getServiceUrl: botUrl => this.getServiceUrl(getSettings().framework.tunnelUrl),
getServiceUrlForOAuth: () => this.getServiceUrl(getSettings().framework.tunnelUrl),
});
}
}
@ -91,6 +90,5 @@ export class Emulator {
public async report(conversationId: string, botUrl: string): Promise<void> {
this.server.report(conversationId);
await this.ngrok.report(conversationId, botUrl);
}
}

Просмотреть файл

@ -36,21 +36,12 @@ import * as path from 'path';
import * as url from 'url';
import {
addNotification,
azureLoggedInUserChanged,
isMac,
newNotification,
rememberBounds,
setOpenUrl,
updateNewTunnelInfo,
updateTunnelError,
updateTunnelStatus,
Notification,
PersistentSettings,
SharedConstants,
TunnelError,
TunnelInfo,
TunnelStatus,
} from '@bfemulator/app-shared';
import { app, BrowserWindow, nativeTheme, Rectangle, screen } from 'electron';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
@ -66,13 +57,10 @@ import { dispatch, getSettings, store } from './state/store';
import { TelemetryService } from './telemetry';
import { botListsAreDifferent, ensureStoragePath, saveSettings, writeFile } from './utils';
import { openFileFromCommandLine } from './utils/openFileFromCommandLine';
import { sendNotificationToClient } from './utils/sendNotificationToClient';
import { WindowManager } from './windowManager';
import { ProtocolHandler } from './protocolHandler';
import { WebSocketServer } from './server/webSocketServer';
const genericTunnelError =
'Oops.. Your ngrok tunnel seems to have an error. Please check the Ngrok Status Viewer for more details';
// start app startup timer
const beginStartupTime = Date.now();
@ -156,7 +144,6 @@ class EmulatorApplication {
constructor() {
Emulator.initialize();
this.initializeNgrokListeners();
this.initializeAppListeners();
this.initializeSystemPreferencesListeners();
store.subscribe(this.storeSubscriptionHandler);
@ -171,12 +158,6 @@ class EmulatorApplication {
this.mainBrowserWindow.on('restore', this.rememberCurrentBounds);
}
private initializeNgrokListeners() {
Emulator.getInstance().ngrok.ngrokEmitter.on('onTunnelError', this.onTunnelError);
Emulator.getInstance().ngrok.ngrokEmitter.on('onNewTunnelConnected', this.onNewTunnelConnected);
Emulator.getInstance().ngrok.ngrokEmitter.on('onTunnelStatusPing', this.onTunnelStatusPing);
}
private initializeSystemPreferencesListeners() {
nativeTheme.on('updated', this.onInvertedColorSchemeChanged);
}
@ -265,31 +246,6 @@ class EmulatorApplication {
dispatch(rememberBounds(bounds));
};
private onTunnelStatusPing = async (status: TunnelStatus) => {
dispatch(updateTunnelStatus({ tunnelStatus: status }));
};
private onNewTunnelConnected = async (tunnelInfo: TunnelInfo) => {
dispatch(updateNewTunnelInfo(tunnelInfo));
};
private onTunnelError = async (response: TunnelError) => {
const { Commands } = SharedConstants;
dispatch(updateTunnelError({ ...response }));
const ngrokNotification: Notification = newNotification(genericTunnelError);
dispatch(addNotification(ngrokNotification.id));
this.commandService.call(Commands.Ngrok.OpenStatusViewer, false);
ngrokNotification.addButton('Debug Console', () => {
this.commandService.remoteCall(Commands.Notifications.Remove, ngrokNotification.id);
this.commandService.call(Commands.Ngrok.OpenStatusViewer);
});
await sendNotificationToClient(ngrokNotification, this.commandService);
Emulator.getInstance().ngrok.broadcastNgrokError(genericTunnelError);
};
private onInvertedColorSchemeChanged = () => {
const { theme, availableThemes } = getSettings().windowState;
const themeInfo = availableThemes.find(availableTheme => availableTheme.name === theme);

Просмотреть файл

@ -1,288 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { join } from 'path';
import {
ngrokTunnel,
updateTunnelError,
NgrokTunnelAction,
StatusCheckOnTunnel,
TunnelStatus,
} from '@bfemulator/app-shared';
import { createStore, combineReducers } from 'redux';
import './fetchProxy';
import { intervals, NgrokInstance } from './ngrok';
import { intervalForEachPing } from './state/sagas/ngrokSagas';
const mockExistsSync = jest.fn(() => true);
const mockDispatch = jest.fn();
let mockStore;
jest.mock('./state', () => ({
get dispatch() {
return mockDispatch;
},
get store() {
return mockStore;
},
}));
jest.mock('fs-extra', () => ({}));
jest.mock('@microsoft/bf-chatdown', () => ({}));
const headersMap: Map<string, string> = new Map();
headersMap.set('Server', 'Emulator');
const tunnelResponseGeneric = (status: number, errorBody: string, headers = headersMap) => {
return {
text: async () => errorBody,
status,
headers,
};
};
const mockTunnelStatusResponse = jest.fn(() => tunnelResponseGeneric(200, 'success'));
const connectToNgrokInstance = async (ngrok: NgrokInstance) => {
try {
const result = await ngrok.connect({
addr: 61914,
path: 'Applications/ngrok',
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
proto: 'http',
});
return result;
} catch (e) {
throw e;
}
};
const mockSpawn = {
on: () => void 0,
stdin: { on: () => void 0 },
stdout: {
pause: () => void 0,
on: (type, cb) => {
if (type === 'data') {
cb('t=2019-02-01T14:10:08-0800 lvl=info msg="starting web service" obj=web addr=127.0.0.1:4041');
}
},
removeListener: () => void 0,
},
stderr: { on: () => void 0, pause: () => void 0 },
kill: () => void 0,
};
let mockOk = 0;
jest.mock('child_process', () => ({
spawn: () => mockSpawn,
}));
jest.mock('fs', () => ({
existsSync: () => mockExistsSync(),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
createWriteStream: () => ({
write: jest.fn(),
end: jest.fn(),
}),
}));
jest.mock('./utils/ensureStoragePath', () => ({ ensureStoragePath: () => '' }));
jest.mock('node-fetch', () => {
const ngrokPublicUrl = 'https://d1a2bf16.ngrok.io';
const mockJson = {
name: 'e2cfb800-266f-11e9-bc59-e5847cdee2d1',
uri: '/api/tunnels/e2cfb800-266f-11e9-bc59-e5847cdee2d1',
proto: 'https',
};
Object.defineProperty(mockJson, 'public_url', {
value: ngrokPublicUrl,
});
return async (input, params) => {
switch (input) {
case ngrokPublicUrl:
if (params.method === 'DELETE') {
return {
ok: ++mockOk > 0,
json: async () => mockJson,
text: async () => 'oh noes!',
};
}
return mockTunnelStatusResponse();
default:
return {
ok: ++mockOk > 0,
json: async () => mockJson,
text: async () => 'oh noes!',
};
}
};
});
describe('the ngrok ', () => {
let ngrok: NgrokInstance;
beforeEach(() => {
jest.useRealTimers();
mockDispatch.mockImplementation((args: NgrokTunnelAction<StatusCheckOnTunnel>) =>
args.payload.onTunnelPingSuccess()
);
mockStore = createStore(combineReducers({ ngrokTunnel }));
ngrok = new NgrokInstance();
ngrok.ngrokEmitter.removeAllListeners();
mockOk = 0;
});
afterEach(() => {
ngrok.kill();
jest.useRealTimers();
});
describe('ngrok connect/disconnect operations', () => {
it('should spawn ngrok successfully when the happy path is followed', async () => {
const result = await connectToNgrokInstance(ngrok);
expect(result).toEqual({
inspectUrl: 'http://127.0.0.1:4041',
url: 'https://d1a2bf16.ngrok.io',
});
});
it('should retry if the request to retrieve the ngrok url fails the first time', async () => {
mockOk = -5;
await connectToNgrokInstance(ngrok);
expect(mockOk).toBe(1);
});
it('should disconnect', async () => {
let disconnected = false;
const ret = new Promise<void>(resolve => {
ngrok.ngrokEmitter.on('disconnect', () => {
disconnected = true;
expect(disconnected).toBe(true);
resolve();
});
});
await connectToNgrokInstance(ngrok);
await ngrok.disconnect();
return ret;
});
it('should throw when the number of reties to retrieve the ngrok url are exhausted.', async () => {
mockOk = -101;
let threw = false;
intervals.retry = 1;
try {
await connectToNgrokInstance(ngrok);
} catch (e) {
threw = e;
}
expect(threw.toString()).toBe('Error: oh noes!');
});
it('should throw if it failed to find an ngrok executable at the specified path.', async () => {
mockExistsSync.mockReturnValueOnce(false);
let thrown;
try {
await connectToNgrokInstance(ngrok);
} catch (e) {
thrown = e;
}
expect(thrown).toBeDefined();
});
});
describe('ngrok tunnel heath status check operations', () => {
it('should check tunnel status every minute and report success if tunnel ping was a success.', async () => {
jest.useFakeTimers();
await connectToNgrokInstance(ngrok);
ngrok.ngrokEmitter.on('onTunnelStatusPing', (msg: TunnelStatus) => {
expect(msg).toEqual(TunnelStatus.Active);
});
jest.advanceTimersByTime(intervalForEachPing + 1);
});
// First minute generates a Too many connections error. Second minute the tunnel resets back to an active state
it('Should not emit onTunnel error if ngrok tunnel error state has not changed to prevent notification flooding.', async () => {
//Situation where ngrok saga does the ping and calls onTunnelPingError with status 400. Before the next ping happens dispatching an action to set the state to reflect the same. Hence, no notificaiton flooding.
jest.useFakeTimers();
const tunnelErrorMock = jest.fn();
ngrok.ngrokEmitter.on('onTunnelError', tunnelErrorMock);
mockDispatch.mockImplementation((args: NgrokTunnelAction<StatusCheckOnTunnel>) => {
args.payload.onTunnelPingError({
status: 400,
text: 'Tunnel does not exist',
});
});
await connectToNgrokInstance(ngrok);
mockStore.dispatch(
updateTunnelError({
statusCode: 400,
errorMessage: 'Tunnel does not exist',
})
);
jest.advanceTimersByTime(intervalForEachPing + 1);
expect(tunnelErrorMock).toBeCalledTimes(1);
});
// // First minute generates a Too many connections error for first minute. Second minute the tunnel resets back to an active state
it('Should dynamically check for status change every minute. ', async () => {
jest.useFakeTimers();
mockDispatch.mockImplementationOnce((args: NgrokTunnelAction<StatusCheckOnTunnel>) => {
args.payload.onTunnelPingError({
text: 'Tunnel has too many connections',
status: 422,
});
});
ngrok.ngrokEmitter.on('onTunnelError', err => {
expect(err.statusCode).toEqual(422);
expect(err.errorMessage).toBe('Tunnel has too many connections');
});
await connectToNgrokInstance(ngrok);
const ret = new Promise<void>(resolve => {
ngrok.ngrokEmitter.on('onTunnelStatusPing', (status: TunnelStatus) => {
expect(status).toBe(TunnelStatus.Active);
resolve();
});
});
jest.advanceTimersByTime(60001);
return ret;
});
});
});

Просмотреть файл

@ -1,309 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ChildProcess, spawn } from 'child_process';
import { EventEmitter } from 'events';
import { clearTimeout, setTimeout } from 'timers';
import { platform } from 'os';
import * as path from 'path';
import { existsSync } from 'fs';
import { uniqueId } from '@bfemulator/sdk-shared';
import { checkOnTunnel, TunnelInfo, TunnelStatus } from '@bfemulator/app-shared';
import { intervalForEachPing } from './state/sagas/ngrokSagas';
import { ensureStoragePath, writeFile, writeStream, FileWriteStream } from './utils';
import { PostmanNgrokCollection } from './utils/postmanNgrokCollection';
import { dispatch, store } from './state';
/* eslint-enable @typescript-eslint/no-var-requires */
export interface NgrokOptions {
addr: number;
name: string;
port: number;
proto: 'http' | 'https' | 'tcp' | 'tls';
inspect: boolean;
host_header?: string;
bind_tls?: boolean | 'both';
subdomain?: string;
hostname?: string;
crt?: string;
key?: string;
client_cas?: string;
remote_addr?: string;
}
const defaultOptions: Partial<NgrokOptions> = {
addr: 80,
name: uniqueId(),
proto: 'http',
inspect: true,
};
const bin = 'ngrok' + (platform() === 'win32' ? '.exe' : '');
const addrRegExp = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/;
const logPath: string = path.join(ensureStoragePath(), 'ngrok.log');
const postmanCollectionPath: string = path.join(ensureStoragePath(), 'ngrokCollection.json');
export const intervals = { retry: 200, expirationPoll: 1000 * 60 * 5, expirationTime: 1000 * 60 * 60 * 8 };
export class NgrokInstance {
// Errors should result in the immediate termination of ngrok
// since we have no visibility into the internal state of
// ngrok after the error is received.
public ngrokEmitter = new EventEmitter().on('error', this.kill);
private pendingConnection: Promise<{ url; inspectUrl }>;
private ngrokProcess: ChildProcess;
private ngrokFilePath = '';
private tunnels = {};
private inspectUrl = '';
private intervalForHealthCheck: NodeJS.Timer = null;
private ws: FileWriteStream = null;
private boundCheckTunnelStatus = null;
constructor(newNgrokPath) {
this.boundCheckTunnelStatus = this.checkTunnelStatus.bind(this);
this.ngrokFilePath = newNgrokPath;
}
public running(): boolean {
return this.ngrokProcess && !!this.ngrokProcess.pid;
}
public async connect(opts: Partial<NgrokOptions>): Promise<{ url; inspectUrl }> {
const options = { ...defaultOptions, ...opts } as NgrokOptions;
if (this.pendingConnection) {
return this.pendingConnection;
}
await this.getNgrokInspectUrl(options);
const tunnelInfo: { url; inspectUrl } = await this.runTunnel(options);
this.checkTunnelStatus();
this.intervalForHealthCheck = setInterval(() => this.boundCheckTunnelStatus(), intervalForEachPing);
return tunnelInfo;
}
public async checkTunnelStatus(): Promise<void> {
dispatch(
checkOnTunnel({
onTunnelPingSuccess: () => {
this.ngrokEmitter.emit('onTunnelStatusPing', TunnelStatus.Active);
},
onTunnelPingError: async (response: { text: string; status: number; cancelPingInterval: boolean }) => {
if (store.getState().ngrokTunnel.errors.statusCode === response.status) {
return;
}
const errorMessage = response.text;
if (this.ws) {
this.ws.write('-- Tunnel Error Response --');
this.ws.write(`Status Code: ${response.status}`);
this.ws.write(errorMessage);
this.ws.write('-- End Response --');
}
this.ngrokEmitter.emit('onTunnelError', {
statusCode: response.status,
errorMessage,
});
this.ngrokEmitter.emit('onTunnelStatusPing', TunnelStatus.Error);
if (!response || !response.status || response.cancelPingInterval) {
clearInterval(this.intervalForHealthCheck);
return;
}
},
})
);
}
public async disconnect(url?: string) {
const tunnelsToDisconnect = url ? [this.tunnels[url]] : Object.keys(this.tunnels);
const requests = tunnelsToDisconnect.map(tunnel => fetch(tunnel, { method: 'DELETE' }));
const responses: Response[] = await Promise.all(requests);
responses.forEach(response => {
if (!response.ok || response.status === 204) {
// Not sure why a 204 is a failure
return;
}
delete this.tunnels[response.url];
this.ngrokEmitter.emit('disconnect', response.url);
});
clearInterval(this.intervalForHealthCheck);
}
public kill() {
if (!this.ngrokProcess) {
return;
}
this.ngrokProcess.stdout.pause();
this.ngrokProcess.stderr.pause();
this.ngrokProcess.kill();
this.ngrokProcess = null;
this.tunnels = {};
this.ws.end();
clearInterval(this.intervalForHealthCheck);
}
private async getNgrokInspectUrl(opts: NgrokOptions): Promise<{ inspectUrl: string }> {
this.ws = writeStream(logPath);
if (this.running()) {
return { inspectUrl: this.inspectUrl };
}
this.ngrokProcess = this.spawnNgrok(opts);
// If we do not receive a ready state from ngrok within 40 seconds, emit and reject
this.inspectUrl = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
const message = 'Failed to receive a ready state from ngrok within 40 seconds.';
this.ngrokEmitter.emit('error', message);
reject(message);
}, 40000);
/**
* Look for an address in the many messages
* sent by ngrok or fail if one does not arrive
* in 40 seconds.
*/
const onNgrokData = (data: Buffer) => {
const addr = data.toString().match(addrRegExp);
if (!addr) {
return;
}
clearTimeout(timeout);
this.ngrokProcess.stdout.removeListener('data', onNgrokData);
resolve(`http://${addr[1]}`);
};
this.ngrokProcess.stdout.on('data', onNgrokData);
process.on('exit', () => {
this.kill();
});
});
return { inspectUrl: this.inspectUrl };
}
private updatePostmanCollectionWithNewUrls(inspectUrl: string): void {
const postmanCopy = JSON.stringify(PostmanNgrokCollection);
const collectionWithUrlReplaced = postmanCopy.replace(/127.0.0.1:4040/g, inspectUrl.replace(/(^\w+:|^)\/\//, ''));
writeFile(postmanCollectionPath, collectionWithUrlReplaced);
}
private async runTunnel(opts: NgrokOptions): Promise<{ url: string; inspectUrl: string }> {
let retries = 100;
const url = `${this.inspectUrl}/api/tunnels`;
const body = JSON.stringify(opts);
// eslint-disable-next-line no-constant-condition
while (true) {
const resp = await fetch(url, {
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
},
});
if (!resp.ok) {
const error = await resp.text();
await new Promise(resolve => setTimeout(resolve, ~~intervals.retry));
if (!retries) {
throw new Error(error);
}
retries--;
continue;
}
const result = await resp.json();
const { public_url: publicUrl, uri, msg } = result;
if (!publicUrl) {
throw Object.assign(new Error(msg || 'failed to start tunnel.'), result);
}
this.tunnels[publicUrl] = uri;
if (opts.proto === 'http' && opts.bind_tls) {
this.tunnels[publicUrl.replace('https', 'http')] = uri + ' (http)';
}
this.ngrokEmitter.emit('connect', publicUrl, this.inspectUrl);
this.updatePostmanCollectionWithNewUrls(this.inspectUrl);
const tunnelDetails: TunnelInfo = {
publicUrl,
inspectUrl: this.inspectUrl,
postmanCollectionPath,
logPath,
};
this.ngrokEmitter.emit('onNewTunnelConnected', tunnelDetails);
this.pendingConnection = null;
return { url: publicUrl, inspectUrl: this.inspectUrl };
}
}
private spawnNgrok(opts: NgrokOptions): ChildProcess {
const filename = `${this.ngrokFilePath ? path.basename(this.ngrokFilePath) : bin}`;
const folder = this.ngrokFilePath ? path.dirname(this.ngrokFilePath) : path.join(__dirname, 'bin');
try {
this.ws.write('Ngrok Logger starting');
const args = ['start', '--none', `--log=stdout`];
const ngrokPath = path.join(folder, filename);
if (!existsSync(ngrokPath)) {
throw new Error(
`Could not find ngrok executable at path: ${ngrokPath}. ` +
`Make sure that the correct path to ngrok is configured in the Emulator app settings. ` +
`Ngrok is required to receive a token from the Bot Framework token service.`
);
}
const ngrok = spawn(ngrokPath, args, { cwd: folder });
// Errors are emitted instead of throwing since ngrok is a long running process
ngrok.on('error', e => this.ngrokEmitter.emit('error', e));
ngrok.on('exit', () => {
this.tunnels = {};
clearInterval(this.intervalForHealthCheck);
this.ngrokEmitter.emit('disconnect');
});
ngrok.on('close', () => {
clearInterval(this.intervalForHealthCheck);
this.ngrokEmitter.emit('close');
});
ngrok.stdout.on('data', data => {
this.ws.write(data.toString() + '\n');
});
ngrok.stderr.on('data', (data: Buffer) =>
this.ngrokEmitter.emit('error', () => {
this.ws.write(data.toString());
})
);
return ngrok;
} catch (e) {
throw e;
}
}
}

Просмотреть файл

@ -1,292 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
azureAuthSettings,
framework,
savedBotUrls,
setFrameworkSettings,
windowState,
SettingsImpl,
} from '@bfemulator/app-shared';
import { combineReducers, createStore } from 'redux';
import { NgrokService } from './ngrokService';
import { store } from './state/store';
const mockEmulator = jest.fn(() => {
return {
server: {
serverUrl: 'http://localhost:3000',
serverPort: 8080,
state: {
conversations: {
getConversationIds: () => ['12', '123'],
},
endpoints: {
reset: () => null,
push: () => null,
},
},
},
};
});
jest.mock('./emulator', () => ({
Emulator: {
getInstance: () => mockEmulator(),
},
}));
let mockSettingsStore;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const getMockSettingsStore = () => mockSettingsStore || (mockSettingsStore = mockCreateStore());
const mockCreateStore = () =>
createStore(
combineReducers({
azure: azureAuthSettings,
framework,
savedBotUrls,
windowState,
})
);
const mockSettingsImpl = SettingsImpl;
jest.mock('./state/store', () => ({
get store() {
return getMockSettingsStore();
},
getSettings: function() {
return new mockSettingsImpl(getMockSettingsStore().getState());
},
get dispatch() {
return mockSettingsStore.dispatch;
},
}));
const mockCallsToLog = [];
jest.mock('./main', () => ({
emulatorApplication: {
mainWindow: {
logService: {
logToChat: (...args: any[]) => {
mockCallsToLog.push({ name: 'remoteCall', args });
},
},
},
},
}));
const mockRunning = jest.fn(() => false);
const mockConnect = jest
.fn()
.mockResolvedValue({ url: 'http://fdsfds.ngrok.io', inspectUrl: 'http://fdsfds.ngrok.io' });
jest.mock('./ngrok', () => {
return {
NgrokInstance: jest.fn().mockImplementation(() => {
return {
running: () => mockRunning(),
connect: mockConnect,
kill: () => true,
};
}),
};
});
describe('The ngrokService', () => {
const ngrokService = new NgrokService();
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: true,
ngrokPath: '/usr/bin/ngrok',
};
beforeEach(() => {
store.dispatch(setFrameworkSettings(settings as any));
mockCallsToLog.length = 0;
mockRunning.mockClear();
mockConnect.mockClear();
});
it('should be a singleton', () => {
expect(ngrokService).toBe(new NgrokService());
});
it('should not invoke ngrok for localhost urls', async () => {
const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3030/v3/messages');
expect(serviceUrl).toBe('http://localhost:8080');
});
it('should connect to ngrok when a remote endpoint is used', async () => {
const serviceUrl = await ngrokService.getServiceUrl('http://myBot.someorg:3030/v3/messages');
expect(serviceUrl).toBe('http://fdsfds.ngrok.io');
});
it('should broadcast to each conversation that ngrok has reconnected', async () => {
await ngrokService.getServiceUrl('http://myBot.someorg:3030/v3/messages');
ngrokService.broadcastNgrokReconnected();
expect(mockCallsToLog.length).toBe(8);
});
it('should report its status to the specified conversation when "report()" is called', async () => {
await ngrokService.getServiceUrl('http://myBot.someorg:3030/v3/messages');
await ngrokService.report('12', '');
expect(mockCallsToLog.length).toBe(1);
});
it('should reportNotConfigured() when no ngrokPath is specified', async () => {
(ngrokService as any).ngrokPath = '';
await ngrokService.report('12', '');
expect(mockCallsToLog.length).toBe(3);
expect(mockCallsToLog[0].args[1].payload.text).toBe(
'ngrok not configured (only needed when connecting to remotely hosted bots)'
);
});
it('should use the current ngrok instance for an oauth postback url if already running', async () => {
(ngrokService as any).serviceUrl = 'someServiceUrl';
(ngrokService as any).pendingRecycle = new Promise(resolve => resolve());
mockRunning.mockReturnValueOnce(true);
const serviceUrl = await ngrokService.getServiceUrlForOAuth();
expect(serviceUrl).toBe('someServiceUrl');
(ngrokService as any).serviceUrl = undefined;
(ngrokService as any).pendingRecycle = null;
});
it('should start up a new ngrok process for an oauth postback url', async () => {
mockRunning.mockReturnValueOnce(false);
mockConnect.mockResolvedValueOnce({ url: 'someNgrokServiceUrl' });
const serviceUrl = await ngrokService.getServiceUrlForOAuth();
expect(serviceUrl).toBe('someNgrokServiceUrl');
});
it('should throw if failed to start up a new ngrok process for an oauth postback url', async () => {
mockRunning.mockReturnValueOnce(false);
mockConnect.mockRejectedValueOnce(new Error('Failed to start ngrok.'));
try {
await ngrokService.getServiceUrlForOAuth();
expect(false); // fail test
} catch (e) {
expect(e).toEqual(
new Error(`Failed to connect to ngrok instance for OAuth postback URL: ${new Error('Failed to start ngrok.')}`)
);
}
});
it('should shut down the ngrok oauth instance if it is running', () => {
const mockKill = jest.fn(() => null);
(ngrokService as any).oauthNgrokInstance = { kill: mockKill };
ngrokService.shutDownOAuthNgrokInstance();
expect(mockKill).toHaveBeenCalled();
(ngrokService as any).oauthNgrokInstance = undefined;
});
describe('Conditions where service url returned is an ngrok url', () => {
it('should always return ngrok url if runNgrokAtStartup is selected', async () => {
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: true,
runNgrokAtStartup: true,
ngrokPath: '/usr/bin/ngrok',
};
store.dispatch(setFrameworkSettings(settings as any));
const serviceUrl = await ngrokService.getServiceUrl('http://my-azure.com/speech-bot/');
expect(serviceUrl).toBe('http://fdsfds.ngrok.io');
});
it('should always return ngrok url if remote bot irrespective of options selected', async () => {
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: false,
runNgrokAtStartup: false,
ngrokPath: '/usr/bin/ngrok',
};
store.dispatch(setFrameworkSettings(settings as any));
const serviceUrl = await ngrokService.getServiceUrl('http://my-azure/api/messages');
expect(serviceUrl).toBe('http://fdsfds.ngrok.io');
});
it('should always return ngrok url if bypassNgrokLocalhost is not selected and ngrok is configured', async () => {
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: false,
runNgrokAtStartup: false,
ngrokPath: '/usr/bin/ngrok',
};
store.dispatch(setFrameworkSettings(settings as any));
const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3978');
expect(serviceUrl).toBe('http://fdsfds.ngrok.io');
});
});
describe('Conditions where service url returned is a localhost url', () => {
it('should always return localhost url if local bot and bypassNgrokLocalhost is selected', async () => {
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: true,
runNgrokAtStartup: false,
ngrokPath: '/usr/bin/ngrok',
};
store.dispatch(setFrameworkSettings(settings as any));
const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3978');
expect(serviceUrl).toBe('http://localhost:8080');
});
it('should always return localhost url if bypassNgrokLocalhost is not selected and ngrok is not configured and its a local bot', async () => {
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: false,
runNgrokAtStartup: false,
ngrokPath: '',
};
store.dispatch(setFrameworkSettings(settings as any));
const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3978');
expect(serviceUrl).toBe('http://localhost:8080');
});
it('should always return localhost url if ngrok is not configured', async () => {
const settings = {
locale: 'en-us',
bypassNgrokLocalhost: false,
runNgrokAtStartup: false,
ngrokPath: '',
};
store.dispatch(setFrameworkSettings(settings as any));
const serviceUrl = await ngrokService.getServiceUrl('http://my-azure/api/messages');
expect(serviceUrl).toBe('http://localhost:8080');
});
});
});

Просмотреть файл

@ -1,314 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { EventEmitter } from 'events';
import { FrameworkSettings } from '@bfemulator/app-shared';
import {
appSettingsItem,
exceptionItem,
externalLinkItem,
isLocalHostUrl,
LogItem,
LogLevel,
textItem,
} from '@bfemulator/sdk-shared';
import { Emulator } from './emulator';
import { emulatorApplication } from './main';
import { NgrokInstance } from './ngrok';
import { getSettings } from './state/store';
let ngrokServiceInstance: NgrokService;
export class NgrokService {
// Pass ngrok path to create instance without adding path to
// Ngrok Options for compatibility with Ngrok 3
private ngrok = new NgrokInstance(getSettings().framework.ngrokPath);
private ngrokPath: string;
private serviceUrl: string;
private inspectUrl: string;
private spawnErr: any;
private localhost = 'localhost';
private triedToSpawn: boolean;
private pendingRecycle: Promise<void>;
private oauthNgrokInstance: NgrokInstance;
constructor() {
return ngrokServiceInstance || (ngrokServiceInstance = this); // Singleton
}
public async getServiceUrl(botUrl: string): Promise<string> {
// Ngrok can show as "running" but not have an active session
// with an assigned ngrok url. If a recycle is pending, await
// on it before reporting the service url otherwise, it will
// report the wrong one.
if (this.pendingRecycle) {
await this.pendingRecycle;
}
if (this.isUsingNgrok(botUrl)) {
if (!this.ngrok.running()) {
await this.startup();
}
return this.serviceUrl;
}
// Do not use ngrok
return `http://${this.localhost}:${Emulator.getInstance().server.serverPort}`;
}
// OAuth sign-in flow must always use an ngrok url so that the BF token
// service can deliver the token to the Emulator
public async getServiceUrlForOAuth(): Promise<string> {
// grab the service url from the current ngrok instance if it's running
if (this.pendingRecycle) {
await this.pendingRecycle;
}
if (this.ngrok.running()) {
return this.serviceUrl;
}
// otherwise, we need to spin up an auxillary ngrok instance that we can tear down when the token response comes back
this.oauthNgrokInstance = new NgrokInstance(getSettings().framework.ngrokPath);
const port = Emulator.getInstance().server.serverPort;
const inspectUrl = new Promise<string>(async (resolve, reject) => {
try {
const { url } = await this.oauthNgrokInstance.connect({
addr: port,
});
resolve(url);
} catch (e) {
reject(new Error(`Failed to connect to ngrok instance for OAuth postback URL: ${e}`));
}
});
return inspectUrl;
}
public shutDownOAuthNgrokInstance(): void {
if (this.oauthNgrokInstance) {
this.oauthNgrokInstance.kill();
}
}
public getSpawnStatus = (): { triedToSpawn: boolean; err: any } => ({
triedToSpawn: this.triedToSpawn,
err: this.spawnErr,
});
public async updateNgrokFromSettings(framework: FrameworkSettings) {
this.cacheSettings();
if (this.ngrokPath !== framework.ngrokPath && this.ngrok.running()) {
return this.recycle();
}
}
public recycle(): Promise<void> {
if (this.pendingRecycle) {
return this.pendingRecycle;
}
this.ngrok.kill();
const port = Emulator.getInstance().server.serverPort;
this.ngrokPath = getSettings().framework.ngrokPath;
this.serviceUrl = `http://${this.localhost}:${port}`;
this.inspectUrl = null;
this.spawnErr = null;
this.triedToSpawn = false;
if (this.ngrokPath && this.ngrokPath.length) {
return (this.pendingRecycle = new Promise(async resolve => {
try {
this.triedToSpawn = true;
const { inspectUrl, url } = await this.ngrok.connect({
addr: port,
});
this.serviceUrl = url;
this.inspectUrl = inspectUrl;
} catch (err) {
this.spawnErr = err;
// eslint-disable-next-line no-console
console.error('Failed to spawn ngrok', err);
}
this.pendingRecycle = null;
resolve();
}));
}
return Promise.resolve();
}
public kill(): void {
if (this.ngrok) {
this.ngrok.kill();
}
}
public async pingTunnel(): Promise<void> {
await this.ngrok.checkTunnelStatus();
}
public get ngrokEmitter(): EventEmitter {
return this.ngrok.ngrokEmitter || undefined;
}
public get running(): boolean {
return this.ngrok.running() || false;
}
/** Logs messages signifying that ngrok has reconnected in all active conversations */
public broadcastNgrokReconnected(): void {
const bypassNgrokLocalhost = getSettings().framework.bypassNgrokLocalhost;
const { broadcast } = this;
broadcast(textItem(LogLevel.Debug, 'ngrok reconnected.'));
broadcast(textItem(LogLevel.Debug, `ngrok listening on ${this.serviceUrl}`));
broadcast(textItem(LogLevel.Debug, 'ngrok traffic inspector:'), externalLinkItem(this.inspectUrl, this.inspectUrl));
if (bypassNgrokLocalhost) {
broadcast(textItem(LogLevel.Debug, 'Will bypass ngrok for local addresses'));
} else {
broadcast(textItem(LogLevel.Debug, 'Will use ngrok for local addresses'));
}
}
/** Logs messages signifying that ngrok has reconnected in all active conversations */
public broadcastNgrokError(errorMessage: string): void {
const { broadcast } = this;
broadcast(textItem(LogLevel.Error, errorMessage));
}
/** Logs an item to all open conversations */
public broadcast(...logItems: LogItem[]): void {
const { conversations } = Emulator.getInstance().server.state;
const conversationIds: string[] = conversations.getConversationIds();
conversationIds.forEach(id => {
emulatorApplication.mainWindow.logService.logToChat(id, ...logItems);
});
}
/** Logs items to a single conversation based on current ngrok status */
public async report(conversationId: string, botUrl: string): Promise<void> {
// TODO - localization
await this.getServiceUrl(botUrl);
if (this.spawnErr) {
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(
LogLevel.Error,
'Failed to spawn ngrok. Please go to the Ngrok Status Viewer and download the log file for a more detailed view of the error.'
),
exceptionItem(this.spawnErr)
);
} else if (!this.ngrokPath) {
this.reportNotConfigured(conversationId);
} else if (this.isUsingNgrok(botUrl)) {
this.reportRunning(conversationId);
} else {
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(LogLevel.Debug, 'ngrok configured but not running')
);
}
}
private async startup() {
this.cacheSettings();
await this.recycle();
}
/** Logs messages that tell the user that ngrok isn't configured */
private reportNotConfigured(conversationId: string): void {
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(LogLevel.Debug, 'ngrok not configured (only needed when connecting to remotely hosted bots)')
);
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
externalLinkItem('Connecting to bots hosted remotely', 'https://aka.ms/cnjvpo')
);
emulatorApplication.mainWindow.logService.logToChat(conversationId, appSettingsItem('Edit ngrok settings'));
}
/** Logs messages that tell the user about ngrok's current running status */
private reportRunning(conversationId: string): void {
const bypassNgrokLocalhost = getSettings().framework.bypassNgrokLocalhost;
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(LogLevel.Debug, `ngrok listening on ${this.serviceUrl}`)
);
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(LogLevel.Debug, 'ngrok traffic inspector:'),
externalLinkItem(this.inspectUrl, this.inspectUrl)
);
if (bypassNgrokLocalhost) {
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(LogLevel.Debug, 'Will bypass ngrok for local addresses')
);
} else {
emulatorApplication.mainWindow.logService.logToChat(
conversationId,
textItem(LogLevel.Debug, 'Will use ngrok for local addresses')
);
}
}
private cacheSettings() {
// Get framework from state
const framework = getSettings().framework;
// ensure that a path to ngrok gets set initially
if (!this.ngrokPath && framework.ngrokPath) {
this.ngrokPath = framework.ngrokPath;
}
// Cache host and port
const localhost = framework.localhost || 'localhost';
const parts = localhost.split(':');
let hostname = localhost;
if (parts.length > 0) {
hostname = parts[0].trim();
}
if (parts.length > 1) {
// Ignore port, for now
// port = +parts[1].trim();
}
this.localhost = hostname;
}
private isUsingNgrok(botUrl: string) {
const { bypassNgrokLocalhost, runNgrokAtStartup } = getSettings().framework;
const local = !botUrl || isLocalHostUrl(botUrl);
return runNgrokAtStartup || !local || (local && !bypassNgrokLocalhost);
}
}

Просмотреть файл

@ -71,32 +71,19 @@ jest.mock('./globals', () => ({
setGlobal: () => null,
}));
let mockNgrokPath;
const mockDispatch = jest.fn();
jest.mock('./state/store', () => ({
store: {
dispatch: action => mockDispatch(action),
},
getSettings: () => ({
framework: {
ngrokPath: mockNgrokPath,
},
framework: {},
}),
}));
let mockGetSpawnStatus: any = jest.fn(() => ({ triedToSpawn: true }));
let mockRunningStatus;
const mockRecycle = jest.fn(() => null);
const mockEmulator = {
framework: { serverUrl: 'http://[::]:8090' },
ngrok: {
getSpawnStatus: () => mockGetSpawnStatus(),
ngrokEmitter: {
once: (_eventName, cb) => cb(),
},
recycle: () => mockRecycle(),
running: () => mockRunningStatus,
},
};
jest.mock('./emulator', () => ({
Emulator: {
@ -178,15 +165,12 @@ describe('Protocol handler tests', () => {
},
],
};
mockRunningStatus = true;
mockNgrokPath = 'path/to/ngrok.exe';
mockSendNotificationToClient = jest.fn(() => null);
mockGotReturnValue = {
statusCode: 200,
body: '["activity1", "activity2", "activity3"]',
};
mockRecycle.mockClear();
mockGetSpawnStatus.mockClear();
mockDispatch.mockClear();
});
@ -377,7 +361,7 @@ describe('Protocol handler tests', () => {
});
});
it('should open a bot when ngrok is running', async () => {
it('should open a bot', async () => {
const protocol = {
parsedArgs: {
id: 'someIdOverride',
@ -388,9 +372,6 @@ describe('Protocol handler tests', () => {
const overrides = { endpoint: parseEndpointOverrides(protocol.parsedArgs) };
const overriddenBot = applyBotConfigOverrides(mockOpenedBot, overrides);
// ngrok should be kick-started if it hasn't tried to spawn yet
mockGetSpawnStatus = jest.fn(() => ({ triedToSpawn: false }));
await ProtocolHandler.openBot(protocol);
expect(mockRecycle).toHaveBeenCalled();
@ -409,96 +390,6 @@ describe('Protocol handler tests', () => {
});
});
it('should open a bot when ngrok is configured but not running', async () => {
mockRunningStatus = false;
const protocol = {
parsedArgs: {
id: 'someIdOverride',
path: 'path/to/bot.bot',
secret: 'someSecret',
},
};
const overrides = { endpoint: parseEndpointOverrides(protocol.parsedArgs) };
const overriddenBot = applyBotConfigOverrides(mockOpenedBot, overrides);
await ProtocolHandler.openBot(protocol);
expect(mockCallsMade).toHaveLength(2);
expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Open);
expect(mockCallsMade[0].args).toEqual(['path/to/bot.bot', 'someSecret']);
expect(mockCallsMade[1].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
expect(mockCallsMade[1].args).toEqual([overriddenBot]);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Load);
expect(mockRemoteCallsMade[0].args).toEqual([overriddenBot]);
expect(mockTrackEvent).toHaveBeenCalledWith('bot_open', {
method: 'protocol',
numOfServices: 1,
source: 'path',
});
});
it('should open a bot when ngrok is not configured', async () => {
mockNgrokPath = undefined;
const protocol = {
parsedArgs: {
id: 'someIdOverride',
path: 'path/to/bot.bot',
secret: 'someSecret',
},
};
const overrides = { endpoint: parseEndpointOverrides(protocol.parsedArgs) };
const overriddenBot = applyBotConfigOverrides(mockOpenedBot, overrides);
await ProtocolHandler.openBot(protocol);
expect(mockCallsMade).toHaveLength(2);
expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Open);
expect(mockCallsMade[0].args).toEqual(['path/to/bot.bot', 'someSecret']);
expect(mockCallsMade[1].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
expect(mockCallsMade[1].args).toEqual([overriddenBot]);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Load);
expect(mockRemoteCallsMade[0].args).toEqual([overriddenBot]);
expect(mockTrackEvent).toHaveBeenCalledWith('bot_open', {
method: 'protocol',
numOfServices: 1,
source: 'path',
});
});
it('should throw if ngrok failed to spawn while opening a bot', async () => {
try {
const protocol = {
parsedArgs: {
id: 'someIdOverride',
path: 'path/to/bot.bot',
secret: 'someSecret',
},
};
mockGetSpawnStatus = jest.fn(() => ({ triedToSpawn: true, err: 'Some ngrok error' }));
await ProtocolHandler.openBot(protocol);
} catch (e) {
expect(e).toEqual(new Error('Error while trying to spawn ngrok instance: Some ngrok error'));
}
});
it('should throw if ngrok failed to spawn while opening a livechat', async () => {
try {
const protocol = {
parsedArgs: {
botUrl: 'someUrl',
msaAppId: 'someAppId',
msaPassword: 'somePw',
},
};
mockGetSpawnStatus = jest.fn(() => ({ triedToSpawn: true, err: 'Some ngrok error' }));
await ProtocolHandler.openBot(protocol);
} catch (e) {
expect(e).toEqual(new Error('Error while trying to spawn ngrok instance: Some ngrok error'));
}
});
it('should open a transcript from a url', async () => {
const protocol = {
parsedArgs: { url: 'https://www.test.com/convo1.transcript' },

Просмотреть файл

@ -33,13 +33,7 @@
import * as Path from 'path';
import {
openBotViaUrlAction,
openTranscript,
FrameworkSettings,
newNotification,
SharedConstants,
} from '@bfemulator/app-shared';
import { openBotViaUrlAction, openTranscript, newNotification, SharedConstants } from '@bfemulator/app-shared';
import got from 'got';
import { IEndpointService } from 'botframework-config/lib/schema';
import {
@ -51,8 +45,7 @@ import {
} from '@bfemulator/sdk-shared';
import { Protocol } from './constants';
import { Emulator } from './emulator';
import { getSettings, store } from './state/store';
import { store } from './state/store';
import { sendNotificationToClient } from './utils/sendNotificationToClient';
import { TelemetryService } from './telemetry';
@ -258,53 +251,13 @@ class ProtocolHandlerImpl implements ProtocolHandler {
bot = applyBotConfigOverrides(bot, overrides);
}
const appSettings: FrameworkSettings = getSettings().framework;
if (appSettings.ngrokPath) {
const ngrok = Emulator.getInstance().ngrok;
let ngrokSpawnStatus = ngrok.getSpawnStatus();
// if ngrok hasn't spawned yet, we need to start it up
if (!ngrokSpawnStatus.triedToSpawn) {
await ngrok.recycle();
}
ngrokSpawnStatus = ngrok.getSpawnStatus();
if (ngrokSpawnStatus.triedToSpawn && ngrokSpawnStatus.err) {
throw new Error(`Error while trying to spawn ngrok instance: ${ngrokSpawnStatus.err || ''}`);
}
if (ngrok.running) {
try {
await this.commandService.call(SharedConstants.Commands.Bot.SetActive, bot);
await this.commandService.remoteCall(SharedConstants.Commands.Bot.Load, bot);
} catch (e) {
throw new Error(`(ngrok running) Error occurred while trying to deep link to bot project at ${path}: ${e}`);
}
} else {
// if ngrok hasn't connected yet, wait for it to connect and load the bot
ngrok.ngrokEmitter.once(
'connect',
async (...args: any[]): Promise<void> => {
try {
await this.commandService.call(SharedConstants.Commands.Bot.SetActive, bot);
await this.commandService.remoteCall(SharedConstants.Commands.Bot.Load, bot);
} catch (e) {
throw new Error(
`(ngrok running but not connected) Error occurred while ` +
`trying to deep link to bot project at ${path}: ${e}`
);
}
}
);
}
} else {
try {
await this.commandService.call(SharedConstants.Commands.Bot.SetActive, bot);
await this.commandService.remoteCall(SharedConstants.Commands.Bot.Load, bot);
} catch (e) {
throw new Error(
`(ngrok not configured) Error occurred while trying to deep link to bot project at ${path}: ${e}`
);
}
try {
await this.commandService.call(SharedConstants.Commands.Bot.SetActive, bot);
await this.commandService.remoteCall(SharedConstants.Commands.Bot.Load, bot);
} catch (e) {
throw new Error(`Error occurred while trying to deep link to bot project at ${path}: ${e}`);
}
const numOfServices = bot.services && bot.services.length;
TelemetryService.trackEvent('bot_open', {
method: 'protocol',

Просмотреть файл

@ -41,6 +41,7 @@ import {
networkRequestItem,
networkResponseItem,
} from '@bfemulator/sdk-shared';
import { ipcMain } from 'electron';
import { createServer, plugins, Server, Response, Route } from 'restify';
import CORS from 'restify-cors-middleware2';
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
@ -57,7 +58,6 @@ export interface EmulatorRestServerOptions {
getServiceUrl?: (botUrl: string) => Promise<string>;
getServiceUrlForOAuth?: () => Promise<string>;
logService?: LogService;
shutDownOAuthNgrokInstance?: () => void;
}
export const defaultRestServerOptions: EmulatorRestServerOptions = {
@ -79,10 +79,6 @@ export const defaultRestServerOptions: EmulatorRestServerOptions = {
)
),
logService: new ConsoleLogService(),
shutDownOAuthNgrokInstance: () =>
new Error(
'shutdownOAuthNgrokInstance() has not been configured. Please configure this function by passing it into the EmulatorRestServer constructor via the "options" object.'
),
};
interface ConversationAwareRequest extends Request {
@ -116,7 +112,6 @@ export class EmulatorRestServer {
public logger: Logger;
public options: EmulatorRestServerOptions;
public server: Server;
public shutDownOAuthNgrokInstance: () => void;
public state: ServerState;
public get serverPort(): number {
@ -140,7 +135,6 @@ export class EmulatorRestServer {
this.state = new ServerState(this.options.fetch);
this.getServiceUrl = this.options.getServiceUrl;
this.getServiceUrlForOAuth = this.options.getServiceUrlForOAuth;
this.shutDownOAuthNgrokInstance = this.options.shutDownOAuthNgrokInstance;
return (server = this);
}
@ -161,6 +155,9 @@ export class EmulatorRestServer {
this._serverPort = actualPort;
this._serverUrl = this.server.url;
console.log('Server listens on port', actualPort);
ipcMain.handle('local-server-port', () => {
return actualPort;
});
} catch (e) {
if (e.code === 'EADDRINUSE') {
// eslint-disable-next-line

Просмотреть файл

@ -127,14 +127,12 @@ describe('replyToActivity route middleware', () => {
});
it('should log any exceptions from OAuth signin in generation before posting the activity to the user', async () => {
const ngrokError = new Error('Failed to spawn ngrok');
mockResolveOAuthCards.mockRejectedValueOnce(ngrokError);
createReplyToActivityHandler(mockEmulatorServer)(mockReq, mockRes, mockNext);
// since the middleware is not an async function, wait for the async operations to complete
await new Promise(resolve => setTimeout(resolve, 500));
expect(mockEmulatorServer.logger.logException).toHaveBeenCalledWith('someConversationId', ngrokError);
expect(mockEmulatorServer.logger.logException).toHaveBeenCalledWith('someConversationId');
expect(mockEmulatorServer.logger.logException).toHaveBeenCalledWith(
'someConversationId',
new Error('Falling back to emulated OAuth token.')

Просмотреть файл

@ -56,7 +56,6 @@ describe('tokenResponseHandler', () => {
};
const mockSendTokenResponse = jest.fn().mockResolvedValue({ statusCode: HttpStatus.OK });
const emulatorServer: any = {
shutDownOAuthNgrokInstance: jest.fn(),
state: {
conversations: {
conversationById: jest.fn(() => ({
@ -70,7 +69,6 @@ describe('tokenResponseHandler', () => {
expect(mockSendTokenResponse).toHaveBeenCalledWith(req.body.connectionName, req.body.token, false);
expect(res.send).toHaveBeenCalledWith(HttpStatus.OK, req.body);
expect(emulatorServer.shutDownOAuthNgrokInstance).toHaveBeenCalled();
});
it('should return the status code of sending the token response if it is not 200', async () => {
@ -84,7 +82,6 @@ describe('tokenResponseHandler', () => {
};
const mockSendTokenResponse = jest.fn().mockResolvedValue({ statusCode: HttpStatus.BAD_REQUEST });
const emulatorServer: any = {
shutDownOAuthNgrokInstance: jest.fn(),
state: {
conversations: {
conversationById: jest.fn(() => ({
@ -98,7 +95,6 @@ describe('tokenResponseHandler', () => {
expect(mockSendTokenResponse).toHaveBeenCalledWith(req.body.connectionName, req.body.token, false);
expect(res.send).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(emulatorServer.shutDownOAuthNgrokInstance).toHaveBeenCalled();
});
it('should send an error response if something goes wrong', async () => {
@ -111,7 +107,6 @@ describe('tokenResponseHandler', () => {
send: jest.fn(),
};
const emulatorServer: any = {
shutDownOAuthNgrokInstance: jest.fn(),
state: {
conversations: {
conversationById: jest.fn(() => ({
@ -124,6 +119,5 @@ describe('tokenResponseHandler', () => {
await tokenResponse(req, res);
expect(mockSendErrorResponse).toHaveBeenCalledWith(req, res, null, new Error('Could not send token response.'));
expect(emulatorServer.shutDownOAuthNgrokInstance).toHaveBeenCalled();
});
});

Просмотреть файл

@ -58,9 +58,6 @@ export function createTokenResponseHandler(emulatorServer: EmulatorRestServer) {
res.end();
} catch (e) {
sendErrorResponse(req, res, null, e);
} finally {
// shut down the oauth ngrok instance
emulatorServer.shutDownOAuthNgrokInstance();
}
};
}

Просмотреть файл

@ -37,11 +37,7 @@ import { createInitialReportHandler } from './initialReport';
jest.mock('../../../../emulator', () => ({
Emulator: {
getInstance: () => ({
ngrok: {
report: jest.fn(),
},
}),
getInstance: () => ({}),
},
}));

Просмотреть файл

@ -35,16 +35,13 @@ import { Next, Request, Response } from 'restify';
import { INTERNAL_SERVER_ERROR, OK } from 'http-status-codes';
import { EmulatorRestServer } from '../../../restServer';
import { Emulator } from '../../../../emulator';
/* sends the initial conversation report to the log panel (ngrok and server url) */
/* sends the initial conversation report to the log panel server url */
export function createInitialReportHandler(emulatorServer: EmulatorRestServer) {
return (req: Request, res: Response, next: Next): any => {
const botUrl = req.body;
const { conversationId } = req.params;
try {
emulatorServer.report(conversationId);
Emulator.getInstance().ngrok.report(conversationId, botUrl);
} catch (e) {
res.send(INTERNAL_SERVER_ERROR, e);
return next();

Просмотреть файл

@ -68,14 +68,14 @@ const mockTranscript = [
name: 'User',
},
locale: 'en-us',
serviceUrl: 'https://3a469f6b.ngrok.io',
serviceUrl: 'https://3a469f6b.mytunnel.io',
},
},
{
type: 'activity add',
activity: {
type: 'message',
serviceUrl: 'https://3a469f6b.ngrok.io',
serviceUrl: 'https://3a469f6b.mytunnel.io',
channelId: 'emulator',
from: {
id: '1',
@ -102,7 +102,7 @@ const mockTranscript = [
type: 'activity add',
activity: {
type: 'message',
serviceUrl: 'https://3a469f6b.ngrok.io',
serviceUrl: 'https://3a469f6b.mytunnel.io',
channelId: 'emulator',
from: {
id: '1',
@ -148,7 +148,7 @@ jest.mock('moment', () => () => ({
const mockUserActivity = {
type: 'message',
serviceUrl: 'https://70d0a286.ngrok.io',
serviceUrl: 'https://70d0a286.mytunnel.io',
channelId: 'emulator',
from: {
id: '1',
@ -166,7 +166,7 @@ const mockUserActivity = {
inputHint: 'acceptingInput',
replyToId: '96547340-1f5c-11e9-9b39-f387f690c8a4',
id: null,
} as Activity;
}; // as Activity;
describe('Conversation class', () => {
let botEndpointBotId;
@ -189,7 +189,7 @@ describe('Conversation class', () => {
beforeEach(() => {
botEndpointBotId = 'someBotEndpointBotId';
botEndpoint = new BotEndpoint('123', botEndpointBotId, 'http://ngrok', null, null, null, null, { fetch });
botEndpoint = new BotEndpoint('123', botEndpointBotId, 'http://mytunnel', null, null, null, null, { fetch });
const mockEmulatorServer: any = {
getServiceUrl: jest.fn().mockResolvedValue('http://localhost'),
logger: {

Просмотреть файл

@ -144,15 +144,14 @@ export class Conversation extends EventEmitter {
this.conversationId,
textItem(
LogLevel.Error,
'Error: The bot is remote, but the service URL is localhost.' +
' Without tunneling software you will not receive replies.'
'Error: The bot is remote, but the service URL is localhost. Without tunneling software you will not receive replies.'
)
);
this.emulatorServer.logger.logMessage(
this.conversationId,
externalLinkItem('Connecting to bots hosted remotely', 'https://aka.ms/cnjvpo')
);
this.emulatorServer.logger.logMessage(this.conversationId, appSettingsItem('Configure ngrok'));
this.emulatorServer.logger.logMessage(this.conversationId, appSettingsItem('Configure a tunnel'));
}
const options = {
@ -651,6 +650,6 @@ class DataUrlEncoder {
}
protected shouldBeDataUrl(url: string): boolean {
return url && (isLocalHostUrl(url) || url.indexOf('ngrok') !== -1);
return url && isLocalHostUrl(url);
}
}

Просмотреть файл

@ -69,7 +69,6 @@ describe('The OauthLinkEncoder', () => {
mockArgsSentToFetch.length = 0;
const emulatorServer = {
getServiceUrl: async () => 'http://localhost',
getServiceUrlForOAuth: async () => 'https://ngrok.io/emulator',
state: {
conversations: {
conversationById: () => ({

Просмотреть файл

@ -128,7 +128,7 @@ export class OAuthLinkEncoder {
};
let errorMessage: string;
try {
// we need to make sure that the postback url is accessible from the token server (ngrok)
// we need to make sure that the postback url is accessible from the token server
const emulatorUrl = await this.emulatorServer.getServiceUrlForOAuth();
const url =
'https://api.botframework.com/api/botsignin/GetSignInUrl?state=' +

Просмотреть файл

@ -32,4 +32,3 @@
//
export * from './settingsSagas';
export * from './ngrokSagas';

Просмотреть файл

@ -1,284 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import '../../fetchProxy';
import { applyMiddleware, createStore, Store, combineReducers } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { put } from 'redux-saga/effects';
import {
checkOnTunnel,
ngrokTunnel,
setTimeIntervalSinceLastPing,
updateNewTunnelInfo,
NgrokTunnelAction,
NgrokTunnelActions,
SharedConstants,
StatusCheckOnTunnel,
TunnelError,
TunnelCheckTimeInterval,
} from '@bfemulator/app-shared';
import { RootState } from '../store';
import { ngrokSagas, NgrokSagas, intervalForEachPing, tunnelPingTimeout } from './ngrokSagas';
const { Remove } = SharedConstants.Commands.Notifications;
const headersMap: Map<string, string> = new Map();
headersMap.set('Server', 'Emulator');
const tunnelResponseGeneric = (status: number, errorBody: string, headers = headersMap) => {
return {
text: async () => errorBody,
status,
headers,
};
};
const mockTunnelStatusResponse = jest.fn(() => Promise.resolve(tunnelResponseGeneric(200, 'success')));
jest.mock('node-fetch', () => {
return async (input, params) => mockTunnelStatusResponse();
});
let mockStore: Store<RootState>;
jest.mock('../store', () => ({
get store() {
return mockStore;
},
}));
const sagaMiddleWare = sagaMiddlewareFactory();
describe('The Ngrok Sagas', () => {
beforeEach(() => {
jest.useRealTimers();
mockStore = createStore(
combineReducers({
ngrokTunnel,
}),
applyMiddleware(sagaMiddleWare)
);
sagaMiddleWare.run(ngrokSagas);
mockStore.dispatch(
updateNewTunnelInfo({
inspectUrl: 'http://127.0.0.1:4040',
publicUrl: 'https://d1a2bf16.ngrok.io',
logPath: '',
postmanCollectionPath: '',
})
);
});
it('should call onTunnelPingSuccess', () => {
const onSuccessMock = jest.fn();
const onFailureMock = jest.fn();
const action: NgrokTunnelAction<StatusCheckOnTunnel> = {
payload: {
onTunnelPingSuccess: onSuccessMock,
onTunnelPingError: onFailureMock,
},
type: NgrokTunnelActions.checkOnTunnel,
};
const gen = NgrokSagas.runTunnelStatusHealthCheck(action);
const notificationIds: string[] = ['notification-1', 'notification-2', 'notification-3'];
let result = gen.next();
result = gen.next('https://d1a2bf16.ngrok.io');
result = gen.next();
result = gen.next(notificationIds);
result.value.ALL.forEach((val, index) => {
expect(val.CALL.args[0]).toEqual(Remove);
expect(val.CALL.args[1]).toEqual(notificationIds[index]);
});
result = gen.next();
expect(result.value).toEqual(put(setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.Now)));
result = gen.next();
result = gen.next();
expect(result.value).toEqual(put(setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.FirstInterval)));
result = gen.next();
result = gen.next();
expect(result.value).toEqual(put(setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.SecondInterval)));
expect(onSuccessMock).toHaveBeenCalled();
expect(onFailureMock).not.toHaveBeenCalled();
});
it('should emit ngrok error - Too many connections.', async () => {
const tunnelError: TunnelError = {
statusCode: 429,
errorMessage: 'The tunnel session has violated the rate-limit policy of 20 connections per minute.',
};
mockTunnelStatusResponse.mockReturnValueOnce(
Promise.resolve(tunnelResponseGeneric(tunnelError.statusCode, tunnelError.errorMessage))
);
return new Promise<void>(resolve => {
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: err => {
expect(err.status).toBe(tunnelError.statusCode);
resolve();
},
})
);
});
});
it('should emit ngrok error - No server header present in the response headers.', async () => {
const tunnelError: TunnelError = {
statusCode: 404,
errorMessage: 'Tunnel not found.',
};
mockTunnelStatusResponse.mockReturnValueOnce(
Promise.resolve(tunnelResponseGeneric(tunnelError.statusCode, tunnelError.errorMessage, new Map()))
);
return new Promise<void>(resolve => {
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: err => {
expect(err.status).toBe(tunnelError.statusCode);
resolve();
},
})
);
});
});
it('should emit ngrok error - Tunnel Expired.', async () => {
const tunnelError: TunnelError = {
statusCode: 402,
errorMessage: 'Other generic tunnel errors.',
};
mockTunnelStatusResponse.mockReturnValueOnce(
Promise.resolve(tunnelResponseGeneric(tunnelError.statusCode, tunnelError.errorMessage, new Map()))
);
return new Promise<void>(resolve => {
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: err => {
expect(err.status).toBe(tunnelError.statusCode);
resolve();
},
})
);
});
});
it('should emit ngrok generic error 500.', async () => {
const tunnelError: TunnelError = {
statusCode: 500,
errorMessage: 'Other generic tunnel errors.',
};
mockTunnelStatusResponse.mockReturnValueOnce(
Promise.resolve(tunnelResponseGeneric(tunnelError.statusCode, tunnelError.errorMessage))
);
return new Promise<void>(resolve => {
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: err => {
expect(err.status).toBe(tunnelError.statusCode);
resolve();
},
})
);
});
});
it('take the latest dispatched tunnel ping action always', async () => {
const tunnelError: TunnelError = {
statusCode: 500,
errorMessage: 'Other generic tunnel errors.',
};
mockTunnelStatusResponse.mockReturnValueOnce(
Promise.resolve(tunnelResponseGeneric(tunnelError.statusCode, tunnelError.errorMessage))
);
const onErrorFake = jest.fn();
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: onErrorFake,
})
);
mockTunnelStatusResponse.mockReturnValueOnce(
Promise.resolve(tunnelResponseGeneric(tunnelError.statusCode, tunnelError.errorMessage))
);
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: onErrorFake,
})
);
return new Promise<void>(resolve => {
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: () => {
expect(onErrorFake).not.toHaveBeenCalled();
resolve();
},
onTunnelPingError: jest.fn(),
})
);
});
});
it('should throw onTunnelPing error if the request times out with a status 404', async () => {
jest.useFakeTimers();
mockTunnelStatusResponse.mockImplementationOnce(
() =>
new Promise(resolve => {
//Never resolve this promise forcing the timeout race condition to win in fetchWithTimeout.ts}));
setTimeout(() => {
resolve();
}, intervalForEachPing);
})
);
const ret = new Promise<void>(resolve => {
mockStore.dispatch(
checkOnTunnel({
onTunnelPingSuccess: jest.fn(),
onTunnelPingError: (err: Response) => {
expect(err.status).toBe(404);
resolve();
},
})
);
});
jest.advanceTimersByTime(tunnelPingTimeout);
return ret;
});
});

Просмотреть файл

@ -1,125 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import delay from '@redux-saga/delay-p';
import { takeLatest, select, put, call, all } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import {
NgrokTunnelActions,
NgrokTunnelAction,
SharedConstants,
StatusCheckOnTunnel,
TunnelCheckTimeInterval,
setTimeIntervalSinceLastPing,
} from '@bfemulator/app-shared';
import { fetchWithTimeout } from '../../utils/fetchWithTimeout';
import { RootState } from '../store';
export const intervalForEachPing = 60000;
export const intervalForRefreshedStatus = 20000;
export const tunnelPingTimeout = 59000;
const getPublicUrl = (state: RootState): string => state.ngrokTunnel.publicUrl;
const getStaleNotificationIds = (state: RootState): string[] => state.ngrokTunnel.ngrokNotificationIds;
const pingTunnel = async (
publicUrl: string
): Promise<{ text: string; status: number; cancelPingInterval: boolean }> => {
let cancelPingInterval = false;
try {
//Make sure the request times out before the next ping which happens every minute. Cannot stop the heart beat as the tunnel might pick back up next minute
const response: Response = await fetchWithTimeout(
publicUrl,
{
headers: {
'Content-Type': 'application/json',
},
},
tunnelPingTimeout
);
const isErrorResponse =
response.status === 429 || response.status === 402 || response.status === 500 || !response.headers.get('Server');
if (response.status === 402) {
cancelPingInterval = true;
}
if (isErrorResponse) {
return {
text: await response.text(),
status: response.status,
cancelPingInterval,
};
}
return undefined;
} catch (ex) {
return {
text: 'Tunnel ping has surpassed the acceptable time limit. Looks like it does not exist anymore.',
status: 404,
cancelPingInterval,
};
}
};
export class NgrokSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
public static *runTunnelStatusHealthCheck(action: NgrokTunnelAction<StatusCheckOnTunnel>): IterableIterator<any> {
try {
const publicUrl: string = yield select(getPublicUrl);
const errorOnResponse = yield pingTunnel(publicUrl);
if (errorOnResponse) {
action.payload.onTunnelPingError(errorOnResponse);
} else {
const { commandService } = NgrokSagas;
const { Remove } = SharedConstants.Commands.Notifications;
const ids: string[] = yield select(getStaleNotificationIds);
if (ids.length > 0) {
yield all(ids.map((id: string) => call([commandService, commandService.remoteCall], Remove, id)));
}
action.payload.onTunnelPingSuccess();
}
yield put(setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.Now));
yield delay(intervalForRefreshedStatus);
yield put(setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.FirstInterval));
yield delay(intervalForRefreshedStatus);
yield put(setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.SecondInterval));
} catch (ex) {
action.payload.onTunnelPingError(ex);
}
}
}
export function* ngrokSagas(): IterableIterator<any> {
yield takeLatest(NgrokTunnelActions.checkOnTunnel, NgrokSagas.runTunnelStatusHealthCheck);
}

Просмотреть файл

@ -67,7 +67,6 @@ export class SettingsSagas {
public static *setFramework(action: FrameworkAction<FrameworkSettings>): IterableIterator<any> {
const emulator = Emulator.getInstance();
yield emulator.ngrok.updateNgrokFromSettings(action.payload);
emulator.server.state.locale = action.payload.locale;
yield* SettingsSagas.pushClientAwareSettings();
}

Просмотреть файл

@ -73,13 +73,11 @@ import {
SettingsImpl,
ThemeState,
UpdateState,
NgrokTunnelState,
ngrokTunnel,
} from '@bfemulator/app-shared';
import { loadSettings, getThemes } from '../utils';
import { ngrokSagas, settingsSagas } from './sagas';
import { settingsSagas } from './sagas';
import { forwardToRenderer } from './middleware/forwardToRenderer';
export interface RootState {
@ -100,7 +98,6 @@ export interface RootState {
settings?: Settings;
theme?: ThemeState;
update?: UpdateState;
ngrokTunnel?: NgrokTunnelState;
}
export const DEFAULT_STATE = {
@ -139,13 +136,12 @@ function initStore(): Store<RootState> {
settings: settingsReducer,
theme,
update,
ngrokTunnel,
}),
DEFAULT_STATE,
applyMiddleware(forwardToRenderer, sagaMiddleware)
);
const sagas = [settingsSagas, ngrokSagas];
const sagas = [settingsSagas];
sagas.forEach(saga => sagaMiddleware.run(saga));
// sync the main process store with any updates on the renderer process

Просмотреть файл

@ -35,8 +35,6 @@ import { loadSettings } from './loadSettings';
const mockSettings = {
framework: {
ngrokPath: '',
bypassNgrokLocalhost: true,
stateSizeLimit: 64,
use10Tokens: false,
useCodeValidation: false,

Просмотреть файл

@ -1,153 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
export const PostmanNgrokCollection = {
info: {
_postman_id: '52c791ee-59ed-4c8b-8ffa-5872edeffe2b',
name: 'Emulator Ngrok Postman',
description:
'The ngrok client exposes a REST API that grants programmatic access to:\r\n\r\n- Collect status and metrics information\r\n- Collect and replay captured requests\r\n- Start and stop tunnels dynamically\r\n\r\nhttps://ngrok.com/docs#client-api',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
},
item: [
{
name: 'Root entry',
request: {
method: 'GET',
header: [],
url: {
raw: 'http://127.0.0.1:4040/api',
protocol: 'http',
host: ['127', '0', '0', '1'],
port: '4040',
path: ['api'],
},
},
response: [],
},
{
name: 'List tunnels',
event: [
{
listen: 'prerequest',
script: {
id: '8550ced6-0aa3-4515-a78e-0e5720955ed7',
exec: [''],
type: 'text/javascript',
},
},
{
listen: 'test',
script: {
id: 'fc71302b-f079-4dd4-aac7-043fff315c58',
exec: [
'var jsonData = JSON.parse(responseBody)',
'pm.globals.set("tunnel_name", jsonData.tunnels[0].name);',
],
type: 'text/javascript',
},
},
],
request: {
method: 'GET',
header: [],
url: {
raw: 'http://127.0.0.1:4040/api/tunnels',
protocol: 'http',
host: ['127', '0', '0', '1'],
port: '4040',
path: ['api', 'tunnels'],
},
},
response: [],
},
{
name: 'Create new tunnel',
request: {
method: 'POST',
header: [
{
key: 'Content-Type',
value: 'application/json',
},
],
body: {
mode: 'raw',
raw:
'{\r\n "name": "HTTPS_9090",\r\n "proto": "http",\r\n\t"addr": "9090",\r\n\t"bind_tls": true\t\r\n}',
},
url: {
raw: 'http://127.0.0.1:4040/api/tunnels',
protocol: 'http',
host: ['127', '0', '0', '1'],
port: '4040',
path: ['api', 'tunnels'],
},
},
response: [],
},
{
name: 'Get tunnel details',
request: {
method: 'GET',
header: [],
url: {
raw: 'http://127.0.0.1:4040/api/tunnels/{{tunnel_name}}',
protocol: 'http',
host: ['127', '0', '0', '1'],
port: '4040',
path: ['api', 'tunnels', '{{tunnel_name}}'],
},
},
response: [],
},
{
name: 'Delete tunnel',
request: {
method: 'DELETE',
header: [],
body: {
mode: 'formdata',
formdata: [],
},
url: {
raw: 'http://127.0.0.1:4040/api/tunnels/{{tunnel_name}}',
protocol: 'http',
host: ['127', '0', '0', '1'],
port: '4040',
path: ['api', 'tunnels', '{{tunnel_name}}'],
},
},
response: [],
},
],
protocolProfileBehavior: {},
};

Просмотреть файл

@ -116,6 +116,7 @@ export const SharedConstants = {
SendTyping: 'emulator:send-activity:typing',
SendPing: 'emulator:send-activity:ping',
SendDeleteUserData: 'emulator:send-activity:delete-user-data',
GetServerPort: 'shell:get-server-port',
},
Extension: {
@ -141,13 +142,6 @@ export const SharedConstants = {
GetStoreState: 'store:get-state',
},
Ngrok: {
Reconnect: 'ngrok:reconnect',
KillProcess: 'ngrok:killProcess',
PingTunnel: 'ngrok:pingTunnel',
OpenStatusViewer: 'ngrok:openStatusViewer',
},
Notifications: {
Add: 'notification:add',
Remove: 'notification:remove',
@ -198,7 +192,6 @@ export const SharedConstants = {
CONTENT_TYPE_APP_SETTINGS: 'application/vnd.microsoft.bfemulator.document.appsettings',
CONTENT_TYPE_WELCOME_PAGE: 'application/vnd.microsoft.bfemulator.document.welcome',
CONTENT_TYPE_TRANSCRIPT: 'application/vnd.microsoft.bfemulator.document.transcript',
CONTENT_TYPE_NGROK_DEBUGGER: 'application/vnd.microsoft.bfemulator.document.ngrokDebugger',
},
Channels: {
ReadmeUrl: 'https://raw.githubusercontent.com/Microsoft/BotFramework-Emulator/main/content/CHANNELS.md',
@ -209,7 +202,6 @@ export const SharedConstants = {
DOCUMENT_ID_BOT_SETTINGS: 'bot:settings',
DOCUMENT_ID_WELCOME_PAGE: 'welcome-page',
DOCUMENT_ID_MARKDOWN_PAGE: 'markdown-page',
DOCUMENT_ID_NGROK_DEBUGGER: 'app:ngrokDebugger',
},
EditorKeys: [EDITOR_KEY_PRIMARY, EDITOR_KEY_SECONDARY],
NavBarItems: {

Просмотреть файл

@ -45,7 +45,6 @@ export * from './explorerActions';
export * from './fileActions';
export * from './frameworkSettingsActions';
export * from './navBarActions';
export * from './ngrokTunnelActions';
export * from './notificationActions';
export * from './presentationActions';
export * from './progressIndicatorActions';

Просмотреть файл

@ -1,122 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
updateNewTunnelInfo,
updateTunnelError,
updateTunnelStatus,
NgrokTunnelActions,
TunnelInfo,
TunnelError,
TunnelStatus,
checkOnTunnel,
setTimeIntervalSinceLastPing,
TunnelCheckTimeInterval,
clearAllNotifications,
addNotification,
} from './ngrokTunnelActions';
describe('Ngrok Tunnel Actions', () => {
it('should create an update tunnel info action', () => {
const payload: TunnelInfo = {
publicUrl: 'https://d1a2bf16.ngrok.io',
inspectUrl: 'http://127.0.0.1:4041',
logPath: 'ngrok.log',
postmanCollectionPath: 'postman.json',
};
const action = updateNewTunnelInfo(payload);
expect(action.type).toBe(NgrokTunnelActions.setDetails);
expect(action.payload).toEqual(payload);
});
it('should create a update tunnel error action', () => {
const payload: TunnelError = {
statusCode: 402,
errorMessage: 'Tunnel has expired',
};
const action = updateTunnelError(payload);
expect(action.type).toBe(NgrokTunnelActions.updateOnError);
expect(action.payload).toEqual(payload);
});
it('should create a tunnel status update action', () => {
const mockDate = new Date(1466424490000);
jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
const expectedStatus: TunnelStatus = TunnelStatus.Active;
const action = updateTunnelStatus({
tunnelStatus: expectedStatus,
});
expect(action.type).toBe(NgrokTunnelActions.setStatus);
expect(action.payload.timestamp).toBe(new Date().getTime());
expect(action.payload.status).toBe(expectedStatus);
});
it('should create a tunnel status update action on TunnelError', () => {
const mockDate = new Date(1466424490000);
jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
const expectedStatus: TunnelStatus = TunnelStatus.Error;
const action = updateTunnelStatus({
tunnelStatus: expectedStatus,
});
expect(action.type).toBe(NgrokTunnelActions.setStatus);
expect(action.payload.timestamp).toBe(new Date().getTime());
expect(action.payload.status).toBe(expectedStatus);
});
it('should create a checkOnTunnel action', () => {
const action = checkOnTunnel({
onTunnelPingError: jest.fn(),
onTunnelPingSuccess: jest.fn(),
});
expect(action.type).toBe(NgrokTunnelActions.checkOnTunnel);
});
it('should create a setTimeIntervalSinceLastPing action', () => {
const action = setTimeIntervalSinceLastPing(TunnelCheckTimeInterval.SecondInterval);
expect(action.type).toBe(NgrokTunnelActions.setTimeIntervalSinceLastPing);
expect(action.payload).toBe(TunnelCheckTimeInterval.SecondInterval);
});
it('should create a clear notifications action', () => {
const action = clearAllNotifications();
expect(action.type).toBe(NgrokTunnelActions.clearAllNotifications);
expect(action.payload).toBeNull;
});
it('should create add notification action', () => {
const notificationId = 'notification-1';
const action = addNotification(notificationId);
expect(action.type).toBe(NgrokTunnelActions.addNotification);
expect(action.payload).toBe(notificationId);
});
});

Просмотреть файл

@ -1,149 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { Action } from 'redux';
export enum NgrokTunnelActions {
setDetails = 'NgrokTunnel/SET_DETAILS',
updateOnError = 'NgrokTunnel/TUNNEL_ERROR',
setStatus = 'NgrokTunnel/SET_STATUS',
checkOnTunnel = 'NgrokTunnel/CHECK_ON_TUNNEL',
setTimeIntervalSinceLastPing = 'NgrokTunnel/TIME_INTERVAL_SINCE_LAST_PING',
clearAllNotifications = 'NgrokTunnel/CLEAR_NOTIFICATIONS',
addNotification = 'NgrokTunnel/ADD_NOTIFICATION',
}
export enum TunnelCheckTimeInterval {
// 0 - 20 seconds
Now,
// 20 - 40 seconds
FirstInterval,
// 40 - 60 seconds
SecondInterval,
}
export enum TunnelStatus {
Active,
Inactive,
Error,
}
export interface TunnelInfo {
publicUrl: string;
inspectUrl: string;
logPath: string;
postmanCollectionPath: string;
}
export interface TunnelError {
statusCode: number;
errorMessage: string;
}
export interface TunnelStatusAndTimestamp {
status: TunnelStatus;
timestamp: number;
}
export interface StatusCheckOnTunnel {
onTunnelPingSuccess: (...args) => any;
onTunnelPingError: (...args) => any;
}
export interface NgrokTunnelAction<T> extends Action {
type: NgrokTunnelActions;
payload: T;
}
export type NgrokTunnelPayloadTypes =
| TunnelError
| TunnelInfo
| TunnelStatusAndTimestamp
| StatusCheckOnTunnel
| TunnelCheckTimeInterval
| string;
export function updateNewTunnelInfo(payload: TunnelInfo): NgrokTunnelAction<TunnelInfo> {
return {
type: NgrokTunnelActions.setDetails,
payload,
};
}
export function updateTunnelStatus(payload: {
tunnelStatus: TunnelStatus;
}): NgrokTunnelAction<TunnelStatusAndTimestamp> {
return {
type: NgrokTunnelActions.setStatus,
payload: {
status: payload.tunnelStatus,
timestamp: new Date().getTime(),
},
};
}
export function updateTunnelError(payload: TunnelError): NgrokTunnelAction<TunnelError> {
return {
type: NgrokTunnelActions.updateOnError,
payload,
};
}
export function checkOnTunnel(payload: StatusCheckOnTunnel): NgrokTunnelAction<StatusCheckOnTunnel> {
return {
type: NgrokTunnelActions.checkOnTunnel,
payload,
};
}
export function setTimeIntervalSinceLastPing(
payload: TunnelCheckTimeInterval
): NgrokTunnelAction<TunnelCheckTimeInterval> {
return {
type: NgrokTunnelActions.setTimeIntervalSinceLastPing,
payload,
};
}
export function clearAllNotifications(): NgrokTunnelAction<void> {
return {
type: NgrokTunnelActions.clearAllNotifications,
payload: null,
};
}
export function addNotification(payload: string): NgrokTunnelAction<string> {
return {
type: NgrokTunnelActions.addNotification,
payload,
};
}

Просмотреть файл

@ -50,4 +50,3 @@ export * from './savedBotUrls';
export * from './theme';
export * from './update';
export * from './windowState';
export * from './ngrokTunnel';

Просмотреть файл

@ -1,179 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
TunnelStatus,
NgrokTunnelAction,
TunnelInfo,
NgrokTunnelPayloadTypes,
NgrokTunnelActions,
TunnelError,
TunnelStatusAndTimestamp,
TunnelCheckTimeInterval,
} from '../actions/ngrokTunnelActions';
import { ngrokTunnel, NgrokTunnelState } from './ngrokTunnel';
describe('Ngrok Tunnel reducer', () => {
const DEFAULT_STATE: NgrokTunnelState = {
inspectUrl: 'http://127.0.0.1:4040',
publicUrl: '',
logPath: '',
postmanCollectionPath: '',
errors: {} as TunnelError,
tunnelStatus: TunnelStatus.Inactive,
lastPingedTimestamp: Date.now(),
timeIntervalSinceLastPing: TunnelCheckTimeInterval.Now,
ngrokNotificationIds: [],
};
afterEach(() => {
const emptyAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
type: null,
payload: null,
};
ngrokTunnel({ ...DEFAULT_STATE }, emptyAction);
});
it('Tunnel info should be set from payload', () => {
const payload: TunnelInfo = {
publicUrl: 'https://abc.io/',
inspectUrl: 'http://127.0.0.1:4003',
logPath: 'logger.txt',
postmanCollectionPath: 'postman.json',
};
const setDetailsAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
type: NgrokTunnelActions.setDetails,
payload,
};
const startingState = { ...DEFAULT_STATE };
const endingState = ngrokTunnel(startingState, setDetailsAction);
expect(endingState.publicUrl).toBe(payload.publicUrl);
expect(endingState.logPath).toBe(payload.logPath);
expect(endingState.postmanCollectionPath).toBe(payload.postmanCollectionPath);
expect(endingState.inspectUrl).toBe(payload.inspectUrl);
});
it('Tunnel errors should be set from payload', () => {
const payload: TunnelError = {
statusCode: 422,
errorMessage: '<h1>Too many connections<h1>',
};
const updateErrorAction: NgrokTunnelAction<NgrokTunnelPayloadTypes> = {
type: NgrokTunnelActions.updateOnError,
payload,
};
const startingState = { ...DEFAULT_STATE };
const endingState = ngrokTunnel(startingState, updateErrorAction);
expect(endingState.publicUrl).toBe(DEFAULT_STATE.publicUrl);
expect(endingState.errors.statusCode).toBe(payload.statusCode);
expect(endingState.errors.errorMessage).toBe(payload.errorMessage);
});
it('Last Ping time interval should be set from payload', () => {
const action = {
type: NgrokTunnelActions.setTimeIntervalSinceLastPing,
payload: TunnelCheckTimeInterval.SecondInterval,
};
const startingState = { ...DEFAULT_STATE };
const transientState = ngrokTunnel(startingState, action);
expect(transientState.timeIntervalSinceLastPing).toBe(action.payload);
});
it('should add notifications with add notification and clear should remove all ngrok notifications', () => {
const getNotificationAction = payload => ({
type: NgrokTunnelActions.addNotification,
payload,
});
const startingState = { ...DEFAULT_STATE };
const notifications: string[] = ['notification-1', 'notification-2', 'notification-3', 'notification-4'];
let transientState = ngrokTunnel(startingState, getNotificationAction(notifications[0]));
transientState = ngrokTunnel(transientState, getNotificationAction(notifications[1]));
transientState = ngrokTunnel(transientState, getNotificationAction(notifications[2]));
transientState = ngrokTunnel(transientState, getNotificationAction(notifications[3]));
expect(transientState.ngrokNotificationIds).toEqual(notifications);
transientState = ngrokTunnel(transientState, {
type: NgrokTunnelActions.clearAllNotifications,
payload: null,
});
expect(transientState.ngrokNotificationIds.length).toBe(0);
transientState = ngrokTunnel(transientState, getNotificationAction(notifications[3]));
expect(transientState.ngrokNotificationIds.length).toBe(1);
const finalState = ngrokTunnel(transientState, {
type: NgrokTunnelActions.clearAllNotifications,
payload: null,
});
expect(finalState.ngrokNotificationIds.length).toBe(0);
});
it('Tunnel status should be set from payload', () => {
const payload: TunnelStatusAndTimestamp = {
status: TunnelStatus.Active,
timestamp: Date.now(),
};
const nextPayload: TunnelStatusAndTimestamp = {
status: TunnelStatus.Error,
timestamp: Date.now(),
};
const actions: NgrokTunnelAction<NgrokTunnelPayloadTypes>[] = [
{
type: NgrokTunnelActions.setStatus,
payload,
},
{
type: NgrokTunnelActions.setStatus,
payload: nextPayload,
},
];
const startingState = { ...DEFAULT_STATE };
let transientState = ngrokTunnel(startingState, actions[0]);
expect(transientState.tunnelStatus).toBe(payload.status);
transientState = ngrokTunnel(transientState, actions[1]);
expect(transientState.tunnelStatus).toBe(nextPayload.status);
transientState = ngrokTunnel(transientState, {
type: NgrokTunnelActions.updateOnError,
payload: {
statusCode: 422,
errorMessage: 'Tunnel has too many connections',
},
});
expect(transientState.errors.statusCode).toBe(422);
transientState = ngrokTunnel(transientState, actions[0]);
expect(transientState.tunnelStatus).toEqual(TunnelStatus.Active);
expect(transientState.errors).toEqual({});
});
});

Просмотреть файл

@ -1,123 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// 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
// 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
// the following conditions:
//
// 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,
// 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
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
NgrokTunnelActions,
NgrokTunnelAction,
NgrokTunnelPayloadTypes,
TunnelError,
TunnelStatus,
TunnelStatusAndTimestamp,
TunnelInfo,
TunnelCheckTimeInterval,
} from '../actions/ngrokTunnelActions';
export interface NgrokTunnelState {
errors: TunnelError;
publicUrl: string;
inspectUrl: string;
logPath: string;
postmanCollectionPath: string;
tunnelStatus: TunnelStatus;
lastPingedTimestamp: number;
timeIntervalSinceLastPing: TunnelCheckTimeInterval;
ngrokNotificationIds: string[];
}
const DEFAULT_STATE: NgrokTunnelState = {
inspectUrl: 'http://127.0.0.1:4040',
publicUrl: '',
logPath: '',
postmanCollectionPath: '',
errors: {} as TunnelError,
tunnelStatus: TunnelStatus.Inactive,
lastPingedTimestamp: Date.now(),
timeIntervalSinceLastPing: TunnelCheckTimeInterval.Now,
ngrokNotificationIds: [],
};
export const ngrokTunnel = (
state: NgrokTunnelState = DEFAULT_STATE,
action: NgrokTunnelAction<NgrokTunnelPayloadTypes>
): NgrokTunnelState => {
switch (action.type) {
case NgrokTunnelActions.setDetails: {
const payload: TunnelInfo = action.payload as TunnelInfo;
state = {
...state,
...payload,
};
break;
}
case NgrokTunnelActions.updateOnError:
state = {
...state,
errors: action.payload as TunnelError,
};
break;
case NgrokTunnelActions.setStatus:
state = {
...state,
tunnelStatus: (action.payload as TunnelStatusAndTimestamp).status,
lastPingedTimestamp: (action.payload as TunnelStatusAndTimestamp).timestamp,
};
if (state.tunnelStatus !== TunnelStatus.Error && state.errors) {
state.errors = {} as TunnelError;
}
break;
case NgrokTunnelActions.setTimeIntervalSinceLastPing:
state = {
...state,
timeIntervalSinceLastPing: action.payload as TunnelCheckTimeInterval,
};
break;
case NgrokTunnelActions.clearAllNotifications:
state = {
...state,
ngrokNotificationIds: [],
};
break;
case NgrokTunnelActions.addNotification:
state = {
...state,
ngrokNotificationIds: [...state.ngrokNotificationIds, action.payload as string],
};
break;
}
return state;
};

Просмотреть файл

@ -34,12 +34,6 @@
import { Bot } from './botTypes';
export interface FrameworkSettings {
// path to use for ngrok
ngrokPath?: string;
// option for deciding whether to bypass ngrok for bots on localhost
bypassNgrokLocalhost?: boolean;
// option to run ngrok when the emulator starts
runNgrokAtStartup?: boolean;
stateSizeLimit?: number;
// option for using 2.0 or 1.0 tokens
use10Tokens?: boolean;
@ -63,6 +57,9 @@ export interface FrameworkSettings {
userGUID?: string;
// use custom user id
useCustomId?: boolean;
// tunnel Url
tunnelUrl?: string;
localPort?: number;
}
export interface WindowStateSettings {
@ -117,9 +114,6 @@ export class SettingsImpl implements Settings {
}
export const frameworkDefault: FrameworkSettings = {
ngrokPath: '',
bypassNgrokLocalhost: true,
runNgrokAtStartup: false,
stateSizeLimit: 64,
use10Tokens: false,
useCodeValidation: false,
@ -131,6 +125,8 @@ export const frameworkDefault: FrameworkSettings = {
hasBeenShownDataCollectionModal: false,
userGUID: '',
useCustomId: false,
tunnelUrl: '',
localPort: 0,
};
export const windowStateDefault: WindowStateSettings = {

Просмотреть файл

@ -43,18 +43,6 @@ export const mockChatLogs = [
],
timestamp: 1561995656213,
},
{
items: [
{
payload: {
level: 0,
text: 'ngrok not configured (only needed when connecting to remotely hosted bots)',
},
type: 'text',
},
],
timestamp: 1561995656213,
},
{
items: [
{
@ -67,17 +55,6 @@ export const mockChatLogs = [
],
timestamp: 1561995656213,
},
{
items: [
{
payload: {
text: 'Edit ngrok settings',
},
type: 'open-app-settings',
},
],
timestamp: 1561995656214,
},
{
items: [
{

Просмотреть файл

@ -44,7 +44,6 @@ export enum LogItemType {
SummaryText = 'summary-text',
OpenAppSettings = 'open-app-settings',
Exception = 'exception',
NgrokExpiration = 'ngrok-expiration',
LuisEditorDeepLink = 'luis-editor-deep-link',
}
export type LogItemPayload =
@ -56,7 +55,6 @@ export type LogItemPayload =
| SummaryTextLogItem
| OpenAppSettingsLogItem
| ExceptionLogItem
| NgrokExpirationLogItem
| LuisEditorDeepLinkLogItem;
export interface LogItem<T = LogItemPayload> {
@ -110,7 +108,3 @@ export interface LuisEditorDeepLinkLogItem {
export interface ExceptionLogItem {
err: any; // Shape of `Error`, but enumerable
}
export interface NgrokExpirationLogItem {
text: string;
}

Просмотреть файл

@ -42,7 +42,6 @@ import {
networkRequestItem,
networkResponseItem,
logEntry,
ngrokExpirationItem,
makeEnumerableObject,
luisEditorDeepLinkItem,
} from './util';
@ -128,12 +127,6 @@ describe('utils tests', () => {
expect(entry.items[1]).toBe(item2);
});
test('ngrokExpirationItem', () => {
const item = ngrokExpirationItem('someText');
expect(item.type).toBe('ngrok-expiration');
expect(item.payload).toEqual({ text: 'someText' });
});
test('luisEditorDeepLink', () => {
const item = luisEditorDeepLinkItem('someText');
expect(item.type).toBe('luis-editor-deep-link');

Просмотреть файл

@ -44,7 +44,6 @@ import {
LuisEditorDeepLinkLogItem,
NetworkRequestLogItem,
NetworkResponseLogItem,
NgrokExpirationLogItem,
OpenAppSettingsLogItem,
SummaryTextLogItem,
TextLogItem,
@ -159,15 +158,6 @@ export function networkResponseItem(
};
}
export function ngrokExpirationItem(text: string): LogItem<NgrokExpirationLogItem> {
return {
type: LogItemType.NgrokExpiration,
payload: {
text,
},
};
}
export function logEntry(...items: LogItem<LogItemPayload>[]): LogEntry {
return {
timestamp: Date.now(),

Просмотреть файл

@ -15,7 +15,6 @@
"noImplicitThis": false,
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"declaration": true,