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:
Родитель
e554dc4231
Коммит
ffc00bd09e
|
@ -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>
|
||||
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>
|
||||
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>
|
||||
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
|
||||
<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,
|
||||
|
|
Загрузка…
Ссылка в новой задаче