WIP: Ngrok Debugger Implementation (#2032)
* Basic ngrok debugger setup * Code cleanup * Renaming functions, test names, using shorthands wherever applicable * Pr feedback addressed
This commit is contained in:
Родитель
d496897b5a
Коммит
cea9a86b8f
|
@ -11,3 +11,4 @@ build/
|
|||
.vscode/*
|
||||
chrome-driver-log.txt
|
||||
.env
|
||||
/packages/**/package-lock.json
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -15,7 +15,8 @@
|
|||
"lint:fix": "npm run lint -- --fix",
|
||||
"start": "run-s build:shared:dev webpackdevServer:dev",
|
||||
"webpackdevServer:dev": "webpack-dev-server --mode development --hot --inline --progress --colors --content-base ./public",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "../../../../testSetup.js",
|
||||
|
@ -51,8 +52,8 @@
|
|||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@types/enzyme": "^3.1.10",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/react": "~16.3.2",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"@types/react": "16.9.17",
|
||||
"@types/react-dom": "16.9.4",
|
||||
"@types/request": "^2.47.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "24.8.0",
|
||||
|
@ -111,8 +112,8 @@
|
|||
"botframework-webchat-core": "4.7.1",
|
||||
"eslint-plugin-react": "^7.12.3",
|
||||
"markdown-it": "^8.4.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
|
|
|
@ -39,11 +39,13 @@ 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_RESOURCES = 'navbar.resources';
|
||||
export const NAVBAR_NGROK_DEBUGGER = 'navbar.ngrokDebugger';
|
||||
|
||||
export const EDITOR_KEY_PRIMARY = 'primary';
|
||||
export const EDITOR_KEY_SECONDARY = 'secondary';
|
||||
|
|
|
@ -39,3 +39,4 @@ export * from './savedBotUrlsActions';
|
|||
export * from './updateActions';
|
||||
export * from './userActions';
|
||||
export * from './windowStateActions';
|
||||
export * from './ngrokTunnelActions';
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// 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,
|
||||
} 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(expectedStatus);
|
||||
expect(action.type).toBe(NgrokTunnelActions.setStatus);
|
||||
expect(action.payload.timestamp).toBe(new Date().toLocaleString());
|
||||
expect(action.payload.status).toBe(expectedStatus);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// 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/STATUS_CHECK',
|
||||
}
|
||||
|
||||
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: string;
|
||||
}
|
||||
|
||||
export interface NgrokTunnelAction<T> extends Action {
|
||||
type: NgrokTunnelActions;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export type NgrokTunnelPayloadTypes = TunnelError | TunnelInfo | TunnelStatusAndTimestamp;
|
||||
|
||||
export function updateNewTunnelInfo(payload: TunnelInfo): NgrokTunnelAction<TunnelInfo> {
|
||||
return {
|
||||
type: NgrokTunnelActions.setDetails,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTunnelStatus(tunnelStatus: TunnelStatus): NgrokTunnelAction<TunnelStatusAndTimestamp> {
|
||||
return {
|
||||
type: NgrokTunnelActions.setStatus,
|
||||
payload: {
|
||||
status: tunnelStatus,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTunnelError(payload: TunnelError): NgrokTunnelAction<TunnelError> {
|
||||
return {
|
||||
type: NgrokTunnelActions.updateOnError,
|
||||
payload,
|
||||
};
|
||||
}
|
|
@ -51,3 +51,4 @@ export * from './theme';
|
|||
export * from './update';
|
||||
export * from './users';
|
||||
export * from './windowState';
|
||||
export * from './ngrokTunnel';
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// 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,
|
||||
TunnelStatusAndTs,
|
||||
} 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,
|
||||
lastTunnelStatusCheckTS: '',
|
||||
};
|
||||
|
||||
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('Tunnel status should be set from payload', () => {
|
||||
const payload: TunnelStatusAndTs = {
|
||||
status: TunnelStatus.Active,
|
||||
ts: '12/27/2019, 1:30:00 PM',
|
||||
};
|
||||
const nextPayload: TunnelStatusAndTs = {
|
||||
status: TunnelStatus.Error,
|
||||
ts: '12/27/2019, 1:33:00 PM',
|
||||
};
|
||||
const actions: NgrokTunnelAction<NgrokTunnelPayloadTypes>[] = [
|
||||
{
|
||||
type: NgrokTunnelActions.setStatus,
|
||||
payload,
|
||||
},
|
||||
{
|
||||
type: NgrokTunnelActions.setStatus,
|
||||
payload: nextPayload,
|
||||
},
|
||||
];
|
||||
const startingState = { ...DEFAULT_STATE };
|
||||
const transientState = ngrokTunnel(startingState, actions[0]);
|
||||
expect(transientState.tunnelStatus).toBe(payload.status);
|
||||
|
||||
const finalState = ngrokTunnel(startingState, actions[1]);
|
||||
expect(finalState.tunnelStatus).toBe(nextPayload.status);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// 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,
|
||||
} from '../actions/ngrokTunnelActions';
|
||||
|
||||
export interface NgrokTunnelState {
|
||||
errors: TunnelError;
|
||||
publicUrl: string;
|
||||
inspectUrl: string;
|
||||
logPath: string;
|
||||
postmanCollectionPath: string;
|
||||
tunnelStatus: TunnelStatus;
|
||||
lastPingedTimestamp: string;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: NgrokTunnelState = {
|
||||
inspectUrl: 'http://127.0.0.1:4040',
|
||||
publicUrl: '',
|
||||
logPath: '',
|
||||
postmanCollectionPath: '',
|
||||
errors: {} as TunnelError,
|
||||
tunnelStatus: TunnelStatus.Inactive,
|
||||
lastPingedTimestamp: '',
|
||||
};
|
||||
|
||||
export const ngrokTunnel = (
|
||||
state: NgrokTunnelState = DEFAULT_STATE,
|
||||
action: NgrokTunnelAction<NgrokTunnelPayloadTypes>
|
||||
): NgrokTunnelState => {
|
||||
switch (action.type) {
|
||||
case NgrokTunnelActions.setDetails:
|
||||
state = {
|
||||
...state,
|
||||
...action.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,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return state;
|
||||
};
|
|
@ -73,6 +73,8 @@ import {
|
|||
ResourcesState,
|
||||
ThemeState,
|
||||
UpdateState,
|
||||
ngrokTunnel,
|
||||
NgrokTunnelState,
|
||||
} from './reducers';
|
||||
|
||||
export interface RootState {
|
||||
|
@ -93,6 +95,7 @@ export interface RootState {
|
|||
settings?: Settings;
|
||||
theme?: ThemeState;
|
||||
update?: UpdateState;
|
||||
ngrokTunnel?: NgrokTunnelState;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {};
|
||||
|
@ -132,6 +135,7 @@ function initStore(): Store<RootState> {
|
|||
settings: settingsReducer,
|
||||
theme,
|
||||
update,
|
||||
ngrokTunnel,
|
||||
}),
|
||||
DEFAULT_STATE,
|
||||
storeEnhancer
|
||||
|
|
|
@ -161,7 +161,7 @@ export class BotCreationDialog extends React.Component<BotCreationDialogProps, B
|
|||
<Row align={RowAlignment.Bottom}>
|
||||
<Checkbox label="Azure for US Government" checked={isAzureGov} onChange={this.onChannelServiceChange} />
|
||||
<LinkButton
|
||||
ariaLabel="Learn more about Azure for US Government. "
|
||||
ariaLabel="Learn more about Azure for US Government."
|
||||
className={dialogStyles.dialogLink}
|
||||
linkRole={true}
|
||||
onClick={this.onAzureGovLinkClick}
|
||||
|
|
|
@ -32,13 +32,14 @@
|
|||
//
|
||||
|
||||
import * as React from 'react';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import * as Constants from '../../constants';
|
||||
import { Document } from '../../state/reducers/editor';
|
||||
|
||||
import { MarkdownPage } from './markdownPage/markdownPage';
|
||||
|
||||
import { AppSettingsEditorContainer, EmulatorContainer, WelcomePageContainer } from './index';
|
||||
import { AppSettingsEditorContainer, EmulatorContainer, WelcomePageContainer, NgrokDebuggerContainer } from './index';
|
||||
|
||||
interface EditorFactoryProps {
|
||||
document?: Document;
|
||||
|
@ -70,6 +71,9 @@ export class EditorFactory extends React.Component<EditorFactoryProps> {
|
|||
case Constants.CONTENT_TYPE_MARKDOWN:
|
||||
return <MarkdownPage markdown={document.meta.markdown} onLine={document.meta.onLine} />;
|
||||
|
||||
case SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER:
|
||||
return <NgrokDebuggerContainer documentId={document.documentId} dirty={this.props.document.dirty} />;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -36,3 +36,4 @@ export * from './editor';
|
|||
export * from './panel/panel';
|
||||
export * from './emulator';
|
||||
export * from './welcomePage';
|
||||
export * from './ngrokDebugger/ngrokDebuggerContainer';
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
//
|
||||
// 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 { Column, Row, LinkButton, MediumHeader, SmallHeader } from '@bfemulator/ui-react';
|
||||
|
||||
import { TunnelError, TunnelStatus } from '../../../state';
|
||||
import { GenericDocument } from '../../layout';
|
||||
|
||||
import { NgrokErrorHandler } from './ngrokErrorHandler';
|
||||
import * as styles from './ngrokDebuggerContainer.scss';
|
||||
|
||||
export interface NgrokDebuggerProps {
|
||||
inspectUrl: string;
|
||||
errors: TunnelError;
|
||||
publicUrl: string;
|
||||
logPath: string;
|
||||
postmanCollectionPath: string;
|
||||
tunnelStatus: TunnelStatus;
|
||||
lastPingedTimestamp: string;
|
||||
onAnchorClick: (linkRef: string) => void;
|
||||
onSaveFileClick: (originalFilePath: string, dialogOptions: Electron.SaveDialogOptions) => void;
|
||||
onPingTunnelClick: () => void;
|
||||
onReconnectToNgrokClick: () => void;
|
||||
}
|
||||
|
||||
const getDialogOptions = (title: string, buttonLabel: string = 'Save'): Electron.SaveDialogOptions => ({
|
||||
title,
|
||||
buttonLabel,
|
||||
});
|
||||
|
||||
export const NgrokDebugger = (props: NgrokDebuggerProps) => {
|
||||
const [statusDisplay, setStatusDisplay] = useState(styles.tunnelInactive);
|
||||
|
||||
const convertToAnchorOnClick = (link: string) => {
|
||||
props.onAnchorClick(link);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
switch (props.tunnelStatus) {
|
||||
case TunnelStatus.Active:
|
||||
setStatusDisplay(styles.tunnelActive);
|
||||
break;
|
||||
|
||||
case TunnelStatus.Error:
|
||||
setStatusDisplay(styles.tunnelError);
|
||||
break;
|
||||
|
||||
default:
|
||||
setStatusDisplay(styles.tunnelInactive);
|
||||
break;
|
||||
}
|
||||
}, [props.lastPingedTimestamp]);
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericDocument className={styles.ngrokDebuggerContainer}>
|
||||
<MediumHeader>Ngrok Status Viewer</MediumHeader>
|
||||
<Row>
|
||||
<Column>
|
||||
<section>
|
||||
<SmallHeader>Tunnel Health</SmallHeader>
|
||||
<ul className={styles.tunnelDetailsList}>
|
||||
<li>
|
||||
<legend>Tunnel Status</legend>
|
||||
<span className={[styles.tunnelHealthIndicator, statusDisplay].join(' ')} />
|
||||
<span>{props.lastPingedTimestamp}</span>
|
||||
</li>
|
||||
{props.tunnelStatus !== TunnelStatus.Inactive ? (
|
||||
<li>
|
||||
<LinkButton linkRole={true} onClick={props.onPingTunnelClick}>
|
||||
Click here
|
||||
</LinkButton>
|
||||
to ping the tunnel now
|
||||
</li>
|
||||
) : null}
|
||||
{errorDetailsContainer}
|
||||
</ul>
|
||||
</section>
|
||||
{props.publicUrl ? tunnelConnections : null}
|
||||
</Column>
|
||||
</Row>
|
||||
</GenericDocument>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.tunnel-health-indicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
margin-right: 2px;
|
||||
color: var(--focused-list-item);
|
||||
|
||||
&.tunnel-error {
|
||||
background-color: var(--error-outline);
|
||||
&::before {
|
||||
content: "Error";
|
||||
}
|
||||
}
|
||||
|
||||
&.tunnel-inactive {
|
||||
background-color: var(--info-outline);
|
||||
&::before {
|
||||
content: "Inactive";
|
||||
}
|
||||
}
|
||||
|
||||
&.tunnel-active {
|
||||
background-color: var(--success-bg);
|
||||
&::before {
|
||||
content: "Active";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
packages/app/client/src/ui/editor/ngrokDebugger/ngrokDebuggerContainer.scss.d.ts
поставляемый
Normal file
12
packages/app/client/src/ui/editor/ngrokDebugger/ngrokDebuggerContainer.scss.d.ts
поставляемый
Normal file
|
@ -0,0 +1,12 @@
|
|||
// This is a generated file. Changes are likely to result in being overwritten
|
||||
export const ngrokDebuggerContainer: string;
|
||||
export const tunnelDetailsList: string;
|
||||
export const tunnelHealthIndicator: string;
|
||||
export const tunnelError: string;
|
||||
export const tunnelInactive: string;
|
||||
export const tunnelActive: string;
|
||||
export const errorDetailedViewer: string;
|
||||
export const consoleViewer: string;
|
||||
export const errorWindow: string;
|
||||
export const spacing: string;
|
||||
export const well: string;
|
|
@ -0,0 +1,196 @@
|
|||
//
|
||||
// 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 } from '../../../state/reducers/ngrokTunnel';
|
||||
import {
|
||||
updateNewTunnelInfo,
|
||||
TunnelInfo,
|
||||
TunnelStatus,
|
||||
updateTunnelStatus,
|
||||
updateTunnelError,
|
||||
} from '../../../state/actions/ngrokTunnelActions';
|
||||
|
||||
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 = null;
|
||||
let instance = null;
|
||||
const mockClassesImpl = mockClasses();
|
||||
|
||||
beforeAll(() => {
|
||||
parent = mount(
|
||||
<Provider store={mockStore}>
|
||||
<NgrokDebuggerContainer />
|
||||
</Provider>
|
||||
);
|
||||
wrapper = parent.find(NgrokDebugger);
|
||||
instance = wrapper.instance();
|
||||
});
|
||||
|
||||
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.Inactive));
|
||||
});
|
||||
|
||||
it('should render without errors', () => {
|
||||
mockStore.dispatch(updateTunnelStatus(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.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.Active));
|
||||
expect(wrapper.find(mockClassesImpl.tunnelActive)).toBeDefined();
|
||||
mockStore.dispatch(updateTunnelStatus(TunnelStatus.Error));
|
||||
expect(wrapper.find(mockClassesImpl.tunnelError)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show that tunnel has expired', () => {
|
||||
mockStore.dispatch(updateTunnelStatus(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.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.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.Error));
|
||||
mockStore.dispatch(
|
||||
updateTunnelError({
|
||||
statusCode: -9999,
|
||||
errorMessage: 'Dummy tunnel error',
|
||||
})
|
||||
);
|
||||
expect(wrapper.html().includes('Looks like the ngrok tunnel does not exist anymore.')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// 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 { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import { executeCommand } from '../../../state/actions/commandActions';
|
||||
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: {}): Partial<NgrokDebuggerProps> => {
|
||||
const {
|
||||
inspectUrl,
|
||||
errors,
|
||||
publicUrl,
|
||||
logPath,
|
||||
postmanCollectionPath,
|
||||
tunnelStatus,
|
||||
lastPingedTimestamp: lastTunnelStatusCheckTS,
|
||||
} = state.ngrokTunnel;
|
||||
|
||||
return {
|
||||
inspectUrl,
|
||||
errors,
|
||||
publicUrl,
|
||||
logPath,
|
||||
postmanCollectionPath,
|
||||
tunnelStatus,
|
||||
lastPingedTimestamp: lastTunnelStatusCheckTS,
|
||||
...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);
|
|
@ -0,0 +1,112 @@
|
|||
//
|
||||
// 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>
|
||||
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>
|
||||
Try signing up here
|
||||
<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>
|
||||
Looks like the ngrok tunnel has expired. Try reconnecting to Ngrok or examine the logs for a detailed
|
||||
explanation of the error.
|
||||
<LinkButton
|
||||
ariaLabel="Click here to reconnect to ngrok."
|
||||
linkRole={false}
|
||||
onClick={props.onReconnectToNgrokClick}
|
||||
>
|
||||
Click here to reconnect to ngrok
|
||||
</LinkButton>
|
||||
</legend>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<legend>
|
||||
Looks like the ngrok tunnel does not exist anymore. Try reconnecting to Ngrok or examine the logs for a
|
||||
detailed explanation of the error.
|
||||
<LinkButton
|
||||
ariaLabel="Click here to reconnect to ngrok."
|
||||
linkRole={false}
|
||||
onClick={props.onReconnectToNgrokClick}
|
||||
>
|
||||
Click here to reconnect to ngrok
|
||||
</LinkButton>
|
||||
</legend>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -31,14 +31,13 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { mount } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
|
||||
import * as BotActions from '../../../state/actions/botActions';
|
||||
import { beginAdd } from '../../../state/actions/notificationActions';
|
||||
import { openContextMenuForBot } from '../../../state/actions/welcomePageActions';
|
||||
import { bot } from '../../../state/reducers/bot';
|
||||
import { executeCommand } from '../../../state/actions/commandActions';
|
||||
|
|
До Ширина: | Высота: | Размер: 3.7 KiB После Ширина: | Высота: | Размер: 3.7 KiB |
|
@ -42,6 +42,7 @@ import {
|
|||
CONTENT_TYPE_MARKDOWN,
|
||||
CONTENT_TYPE_TRANSCRIPT,
|
||||
CONTENT_TYPE_WELCOME_PAGE,
|
||||
CONTENT_TYPE_NGROK_DEBUGGER,
|
||||
} from '../../../../constants';
|
||||
import { getOtherTabGroup } from '../../../../state/helpers/editorHelpers';
|
||||
import { Document, Editor } from '../../../../state/reducers/editor';
|
||||
|
@ -285,6 +286,9 @@ export class TabBar extends React.Component<TabBarProps, TabBarState> {
|
|||
case CONTENT_TYPE_DEBUG:
|
||||
return 'Debug';
|
||||
|
||||
case CONTENT_TYPE_NGROK_DEBUGGER:
|
||||
return 'Ngrok Status';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -104,6 +104,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
&: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;
|
||||
|
|
|
@ -129,7 +129,7 @@ describe('<NavBar/>', () => {
|
|||
|
||||
it('should render links for each section', () => {
|
||||
expect(instance).not.toBeNull();
|
||||
expect(instance.links).toHaveLength(4);
|
||||
expect(instance.links).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should render a notification badge', () => {
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface NavBarProps {
|
|||
showExplorer?: (show: boolean) => void;
|
||||
navBarSelectionChanged?: (selection: string) => void;
|
||||
openEmulatorSettings?: () => void;
|
||||
openNgrokDebuggerPanel?: () => void;
|
||||
notifications?: string[];
|
||||
explorerIsVisible?: boolean;
|
||||
botIsOpen?: boolean;
|
||||
|
@ -59,6 +60,7 @@ const selectionMap = [
|
|||
Constants.NAVBAR_RESOURCES,
|
||||
Constants.NAVBAR_NOTIFICATIONS,
|
||||
Constants.NAVBAR_SETTINGS,
|
||||
Constants.NAVBAR_NGROK_DEBUGGER,
|
||||
];
|
||||
|
||||
export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
||||
|
@ -101,7 +103,14 @@ export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
|||
}
|
||||
break;
|
||||
|
||||
// Settings
|
||||
case 3:
|
||||
this.props.openEmulatorSettings();
|
||||
break;
|
||||
|
||||
case 4:
|
||||
this.props.openNgrokDebuggerPanel();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.props.openEmulatorSettings();
|
||||
break;
|
||||
|
@ -112,7 +121,7 @@ export class NavBarComponent extends React.Component<NavBarProps, NavBarState> {
|
|||
const { selection } = this.state;
|
||||
const { explorerIsVisible, botIsOpen = false } = this.props;
|
||||
|
||||
return ['Bot Explorer', 'Resources', 'Notifications', 'Settings'].map((title, index) => {
|
||||
return ['Bot Explorer', 'Resources', 'Notifications', 'Settings', 'Ngrok'].map((title, index) => {
|
||||
return (
|
||||
<button
|
||||
aria-selected={explorerIsVisible && selection === selectionMap[index]}
|
||||
|
|
|
@ -64,6 +64,16 @@ const mapDispatchToProps = (dispatch): NavBarProps => ({
|
|||
},
|
||||
trackEvent: (name: string, properties?: { [key: string]: any }) =>
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
|
||||
openNgrokDebuggerPanel: () => {
|
||||
dispatch(
|
||||
EditorActions.open({
|
||||
contentType: SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER,
|
||||
documentId: SharedConstants.DocumentIds.DOCUMENT_ID_NGROK_DEBUGGER,
|
||||
isGlobal: true,
|
||||
meta: null,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const NavBar = connect(mapStateToProps, mapDispatchToProps)(NavBarComponent);
|
||||
|
|
|
@ -27,6 +27,7 @@ html {
|
|||
--error-bg: #5A1D1D;
|
||||
--error-outline: #BE1100;
|
||||
--endpoint-warning: var(--neutral-14);
|
||||
--success-bg: #47B07F;
|
||||
|
||||
/* webchat overridden colors */
|
||||
/* activity bubbles */
|
||||
|
|
|
@ -24,6 +24,7 @@ html {
|
|||
--error-text: #A1260D;
|
||||
--error-bg: #F2DEDE;
|
||||
--error-outline: #BE1100;
|
||||
--success-bg: #47B07F;
|
||||
--endpoint-warning: var(--neutral-14);
|
||||
|
||||
/* webchat overridden colors */
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -8,6 +8,7 @@
|
|||
"homepage": "https://github.com/Microsoft/BotFramework-Emulator",
|
||||
"scripts": {
|
||||
"build": "run-s typecheck build:electron",
|
||||
"rebuild:keytar:electron": "electron-rebuild",
|
||||
"build:electron": "babel ./src --out-dir app/server --extensions \".ts,.tsx\" --ignore \"**/*.spec.ts\" && npm run copy:extension:stubs",
|
||||
"copy:extension:stubs": "ncp src/extensions app/extensions",
|
||||
"dist": "electron-builder",
|
||||
|
@ -20,6 +21,7 @@
|
|||
"start:react-app": "cd ../client && npm start",
|
||||
"start:watch": "nodemon",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
@ -45,6 +45,7 @@ import { ContextMenuService } from '../services/contextMenuService';
|
|||
import { TelemetryService } from '../telemetry';
|
||||
import { showOpenDialog, showSaveDialog } from '../utils';
|
||||
import { AppUpdater } from '../appUpdater';
|
||||
import { copyFileAsync } from '../utils/copyFileAsync';
|
||||
|
||||
const { shell } = Electron;
|
||||
|
||||
|
@ -193,6 +194,17 @@ export class ElectronCommands {
|
|||
return shell.moveItemToTrash(path.resolve(filePath));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Given source path. Copies it to Destination Path
|
||||
@Command(Commands.CopyFile)
|
||||
protected async copyFile(sourcePath: string, destinationPath: string) {
|
||||
try {
|
||||
await copyFileAsync(sourcePath, destinationPath);
|
||||
} catch (ex) {
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Renames a file - the payload must contain the property "path" and "name"
|
||||
// This will also rename the file extension if one is provided in the "name" field
|
||||
|
|
|
@ -57,4 +57,9 @@ export class NgrokCommands {
|
|||
protected killNgrokProcess() {
|
||||
Emulator.getInstance().ngrok.kill();
|
||||
}
|
||||
|
||||
@Command(Commands.PingTunnel)
|
||||
protected pingForStatusOfTunnel() {
|
||||
Emulator.getInstance().ngrok.pingTunnel();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ jest.mock('electron', () => ({
|
|||
app: {
|
||||
on: () => void 0,
|
||||
setName: () => void 0,
|
||||
getPath: () => '',
|
||||
},
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
|
|
|
@ -46,6 +46,15 @@ import './fetchProxy';
|
|||
import { Window } from './platform/window';
|
||||
import { azureLoggedInUserChanged } from './state/actions/azureAuthActions';
|
||||
import { rememberBounds } from './state/actions/windowStateActions';
|
||||
import {
|
||||
updateTunnelError,
|
||||
TunnelInfo,
|
||||
updateNewTunnelInfo,
|
||||
TunnelStatus,
|
||||
updateTunnelStatus,
|
||||
TunnelError,
|
||||
} from './state/actions/ngrokTunnelActions';
|
||||
import * as EditorActions from './state/actions/editorActions';
|
||||
import { dispatch, getSettings, store } from './state/store';
|
||||
import { TelemetryService } from './telemetry';
|
||||
import { botListsAreDifferent, ensureStoragePath, saveSettings, writeFile } from './utils';
|
||||
|
@ -154,7 +163,9 @@ class EmulatorApplication {
|
|||
}
|
||||
|
||||
private initializeNgrokListeners() {
|
||||
Emulator.getInstance().ngrok.ngrokEmitter.on('expired', this.onNgrokSessionExpired);
|
||||
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() {
|
||||
|
@ -242,27 +253,39 @@ class EmulatorApplication {
|
|||
dispatch(rememberBounds(bounds));
|
||||
};
|
||||
|
||||
// ngrok listeners
|
||||
private onNgrokSessionExpired = async () => {
|
||||
// when ngrok expires, spawn notification to reconnect
|
||||
const ngrokNotification: Notification = newNotification(
|
||||
'Your ngrok tunnel instance has expired. Would you like to reconnect to a new tunnel?'
|
||||
);
|
||||
ngrokNotification.addButton('Dismiss', () => {
|
||||
private onTunnelStatusPing = async (status: TunnelStatus) => {
|
||||
dispatch(updateTunnelStatus(status));
|
||||
};
|
||||
|
||||
private onNewTunnelConnected = async (tunnelInfo: TunnelInfo) => {
|
||||
dispatch(updateNewTunnelInfo(tunnelInfo));
|
||||
};
|
||||
|
||||
private onTunnelError = async (response: TunnelError) => {
|
||||
// Avoid reporting the same error again and again to avoid notification flooding
|
||||
if (store.getState().ngrokTunnel.errors.statusCode === response.statusCode) {
|
||||
return;
|
||||
}
|
||||
const genericTunnelError =
|
||||
'Oops.. Your ngrok tunnel seems to have an error. Please check the Ngrok Debug Console for more details';
|
||||
dispatch(updateTunnelError({ ...response }));
|
||||
|
||||
const ngrokNotification: Notification = newNotification(genericTunnelError);
|
||||
ngrokNotification.addButton('Debug Console', () => {
|
||||
// Go to Ngrok from here
|
||||
const { Commands } = SharedConstants;
|
||||
this.commandService.remoteCall(Commands.Notifications.Remove, ngrokNotification.id);
|
||||
});
|
||||
ngrokNotification.addButton('Reconnect', async () => {
|
||||
try {
|
||||
const { Commands } = SharedConstants;
|
||||
await this.commandService.call(Commands.Ngrok.Reconnect);
|
||||
this.commandService.remoteCall(Commands.Notifications.Remove, ngrokNotification.id);
|
||||
} catch (e) {
|
||||
await sendNotificationToClient(newNotification(e), this.commandService);
|
||||
}
|
||||
dispatch(
|
||||
EditorActions.open({
|
||||
contentType: SharedConstants.ContentTypes.CONTENT_TYPE_NGROK_DEBUGGER,
|
||||
documentId: SharedConstants.DocumentIds.DOCUMENT_ID_NGROK_DEBUGGER,
|
||||
isGlobal: true,
|
||||
meta: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
await sendNotificationToClient(ngrokNotification, this.commandService);
|
||||
Emulator.getInstance().ngrok.broadcastNgrokExpired();
|
||||
Emulator.getInstance().ngrok.broadcastNgrokError(genericTunnelError);
|
||||
};
|
||||
|
||||
private onInvertedColorSchemeChanged = () => {
|
||||
|
@ -316,7 +339,7 @@ class EmulatorApplication {
|
|||
app.on('open-url', this.onAppOpenUrl);
|
||||
};
|
||||
|
||||
private onAppOpenUrl = (event: any, url: string): void => {
|
||||
private onAppOpenUrl = (event: Event, url: string): void => {
|
||||
event.preventDefault();
|
||||
if (isMac()) {
|
||||
protocolUsed = true;
|
||||
|
|
|
@ -30,162 +30,265 @@
|
|||
// 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 './fetchProxy';
|
||||
import { intervals, NgrokInstance } from './ngrok';
|
||||
import { TunnelStatus, TunnelError } from './state/actions/ngrokTunnelActions';
|
||||
|
||||
const mockExistsSync = jest.fn(() => true);
|
||||
|
||||
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: () => {},
|
||||
stdin: { on: (type, cb) => void 0 },
|
||||
stdin: { on: () => void 0 },
|
||||
stdout: {
|
||||
pause: () => void 0,
|
||||
on: (type, cb) => {
|
||||
if (type === 'data') {
|
||||
setTimeout(() =>
|
||||
cb('t=2019-02-01T14:10:08-0800 lvl=info msg="starting web service" obj=web addr=127.0.0.1:4041')
|
||||
);
|
||||
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: (type, cb) => void 0, pause: () => void 0 },
|
||||
stderr: { on: () => void 0, pause: () => void 0 },
|
||||
kill: () => void 0,
|
||||
};
|
||||
|
||||
let mockOk = 0;
|
||||
jest.mock('child_process', () => ({
|
||||
spawn: () => mockSpawn,
|
||||
}));
|
||||
|
||||
let mockOk = 0;
|
||||
jest.mock('fs', () => ({
|
||||
existsSync: () => mockExistsSync(),
|
||||
createWriteStream: () => ({
|
||||
write: jest.fn(),
|
||||
end: jest.fn(),
|
||||
}),
|
||||
mkdirSync: jest.fn(),
|
||||
writeFileSync: 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: 'https://d1a2bf16.ngrok.io',
|
||||
value: ngrokPublicUrl,
|
||||
});
|
||||
return async (input, init) => {
|
||||
return {
|
||||
ok: ++mockOk > 0,
|
||||
json: async () => mockJson,
|
||||
text: async () => 'oh noes!',
|
||||
};
|
||||
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!',
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const mockExistsSync = jest.fn(() => true);
|
||||
jest.mock('fs', () => ({
|
||||
existsSync: () => mockExistsSync(),
|
||||
}));
|
||||
|
||||
describe('the ngrok ', () => {
|
||||
const ngrok = new NgrokInstance();
|
||||
let ngrok: NgrokInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
ngrok = new NgrokInstance();
|
||||
mockOk = 0;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ngrok.kill();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should spawn ngrok successfully when the happy path is followed', async () => {
|
||||
const result = await ngrok.connect({
|
||||
addr: 61914,
|
||||
path: '/Applications/ngrok',
|
||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||
proto: 'http',
|
||||
});
|
||||
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 ngrok.connect({
|
||||
addr: 61914,
|
||||
path: '/Applications/ngrok',
|
||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||
proto: 'http',
|
||||
});
|
||||
|
||||
expect(mockOk).toBe(1);
|
||||
});
|
||||
|
||||
it('should emit when the ngrok session is expired', async () => {
|
||||
mockOk = 0;
|
||||
intervals.retry = 100;
|
||||
intervals.expirationPoll = 1;
|
||||
intervals.expirationTime = -1;
|
||||
let emitted = false;
|
||||
ngrok.ngrokEmitter.on('expired', () => {
|
||||
emitted = true;
|
||||
});
|
||||
await ngrok.connect({
|
||||
addr: 61914,
|
||||
path: '/Applications/ngrok',
|
||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||
proto: 'http',
|
||||
});
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
expect(emitted).toBe(true);
|
||||
});
|
||||
|
||||
it('should disconnect', async () => {
|
||||
let disconnected = false;
|
||||
ngrok.ngrokEmitter.on('disconnect', url => {
|
||||
disconnected = true;
|
||||
});
|
||||
|
||||
await ngrok.connect({
|
||||
addr: 61914,
|
||||
path: '/Applications/ngrok',
|
||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||
proto: 'http',
|
||||
});
|
||||
await ngrok.disconnect();
|
||||
expect(disconnected).toBe(true);
|
||||
});
|
||||
|
||||
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 ngrok.connect({
|
||||
addr: 61914,
|
||||
path: '/Applications/ngrok',
|
||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||
proto: 'http',
|
||||
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',
|
||||
});
|
||||
} catch (e) {
|
||||
threw = e;
|
||||
}
|
||||
expect(threw.toString()).toBe('Error: oh noes!');
|
||||
});
|
||||
|
||||
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 done => {
|
||||
let disconnected = false;
|
||||
ngrok.ngrokEmitter.on('disconnect', () => {
|
||||
disconnected = true;
|
||||
expect(disconnected).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
await connectToNgrokInstance(ngrok);
|
||||
await ngrok.disconnect();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const path = join('Applications', 'ngrok');
|
||||
let thrown;
|
||||
try {
|
||||
await connectToNgrokInstance(ngrok);
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
expect(thrown.toString()).toBe(
|
||||
`Error: Could not find ngrok executable at path: ${path}. 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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if it failed to find an ngrok executable at the specified path', async () => {
|
||||
mockExistsSync.mockReturnValueOnce(false);
|
||||
|
||||
const path = join('Applications', 'ngrok');
|
||||
let thrown;
|
||||
try {
|
||||
await ngrok.connect({
|
||||
addr: 61914,
|
||||
path,
|
||||
name: 'c87d3e60-266e-11e9-9528-5798e92fee89',
|
||||
proto: 'http',
|
||||
describe('ngrok tunnel heath status check operations', () => {
|
||||
it('should emit ngrok error - Too many connections.', async done => {
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||
tunnelResponseGeneric(
|
||||
429,
|
||||
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||
)
|
||||
);
|
||||
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||
expect(error.statusCode).toBe(429);
|
||||
done();
|
||||
});
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
await connectToNgrokInstance(ngrok);
|
||||
});
|
||||
|
||||
expect(thrown.toString()).toBe(
|
||||
`Error: Could not find ngrok executable at path: ${path}. 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.`
|
||||
);
|
||||
it('should emit ngrok error - Tunnel Expired.', async done => {
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(tunnelResponseGeneric(402, 'Tunnel has expired beyond the 8 hrs.'));
|
||||
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||
expect(error.statusCode).toBe(402);
|
||||
done();
|
||||
});
|
||||
await connectToNgrokInstance(ngrok);
|
||||
});
|
||||
|
||||
it('should emit ngrok error - No server header present in the response headers.', async done => {
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(tunnelResponseGeneric(404, 'Tunnel not found.', new Map()));
|
||||
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||
expect(error.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
await connectToNgrokInstance(ngrok);
|
||||
});
|
||||
|
||||
it('should emit ngrok error - Tunnel has errored out.', async done => {
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(tunnelResponseGeneric(500, 'Tunnel has errored out.'));
|
||||
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||
expect(error.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
await connectToNgrokInstance(ngrok);
|
||||
});
|
||||
|
||||
it('should emit ngrok error - Too many connections.', async done => {
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||
tunnelResponseGeneric(
|
||||
429,
|
||||
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||
)
|
||||
);
|
||||
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||
expect(error.statusCode).toBe(429);
|
||||
done();
|
||||
});
|
||||
await connectToNgrokInstance(ngrok);
|
||||
});
|
||||
|
||||
it('should check tunnel status every minute and report error', async done => {
|
||||
jest.useFakeTimers();
|
||||
ngrok.ngrokEmitter.on('onTunnelError', (error: TunnelError) => {
|
||||
expect(error.statusCode).toBe(429);
|
||||
done();
|
||||
});
|
||||
await connectToNgrokInstance(ngrok);
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||
tunnelResponseGeneric(
|
||||
429,
|
||||
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||
)
|
||||
);
|
||||
jest.advanceTimersByTime(60001);
|
||||
});
|
||||
|
||||
it('should emit onTunnelStatusPing with an error status', async done => {
|
||||
jest.useFakeTimers();
|
||||
await connectToNgrokInstance(ngrok);
|
||||
mockTunnelStatusResponse.mockReturnValueOnce(
|
||||
tunnelResponseGeneric(
|
||||
429,
|
||||
'The tunnel session has violated the rate-limit policy of 20 connections per minute.'
|
||||
)
|
||||
);
|
||||
ngrok.ngrokEmitter.on('onTunnelStatusPing', (val: TunnelStatus) => {
|
||||
expect(val).toBe(TunnelStatus.Error);
|
||||
done();
|
||||
});
|
||||
jest.advanceTimersByTime(60001);
|
||||
});
|
||||
|
||||
it('should check tunnel status every minute.', async done => {
|
||||
jest.useFakeTimers();
|
||||
await connectToNgrokInstance(ngrok);
|
||||
ngrok.ngrokEmitter.on('onTunnelStatusPing', (msg: TunnelStatus) => {
|
||||
expect(msg).toEqual(TunnelStatus.Active);
|
||||
done();
|
||||
});
|
||||
jest.advanceTimersByTime(60001);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,13 +32,17 @@
|
|||
//
|
||||
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 { clearTimeout, setTimeout } from 'timers';
|
||||
|
||||
import { uniqueId } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { TunnelInfo, TunnelStatus } from './state/actions/ngrokTunnelActions';
|
||||
import { ensureStoragePath, writeFile, writeStream, FileWriteStream } from './utils';
|
||||
import { PostmanNgrokCollection } from './utils/postmanNgrokCollection';
|
||||
|
||||
/* eslint-enable typescript/no-var-requires */
|
||||
export interface NgrokOptions {
|
||||
addr: number;
|
||||
|
@ -68,6 +72,8 @@ const defaultOptions: Partial<NgrokOptions> = {
|
|||
|
||||
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 };
|
||||
|
||||
|
@ -80,8 +86,13 @@ export class NgrokInstance {
|
|||
private ngrokProcess: ChildProcess;
|
||||
private tunnels = {};
|
||||
private inspectUrl = '';
|
||||
private ngrokStartTime: number;
|
||||
private ngrokExpirationTimer: NodeJS.Timer;
|
||||
private intervalForHealthCheck: NodeJS.Timer = null;
|
||||
private ws: FileWriteStream = null;
|
||||
private boundCheckTunnelStatus = null;
|
||||
|
||||
constructor() {
|
||||
this.boundCheckTunnelStatus = this.checkTunnelStatus.bind(this);
|
||||
}
|
||||
|
||||
public running(): boolean {
|
||||
return this.ngrokProcess && !!this.ngrokProcess.pid;
|
||||
|
@ -93,7 +104,30 @@ export class NgrokInstance {
|
|||
return this.pendingConnection;
|
||||
}
|
||||
await this.getNgrokInspectUrl(options);
|
||||
return this.runTunnel(options);
|
||||
const tunnelInfo: { url; inspectUrl } = await this.runTunnel(options);
|
||||
this.intervalForHealthCheck = setInterval(() => this.boundCheckTunnelStatus(tunnelInfo.url), 60000);
|
||||
return tunnelInfo;
|
||||
}
|
||||
|
||||
public async checkTunnelStatus(publicUrl: string): Promise<void> {
|
||||
const response: Response = await fetch(publicUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const isErrorResponse =
|
||||
response.status === 429 || response.status === 402 || response.status === 500 || !response.headers.get('Server');
|
||||
if (isErrorResponse) {
|
||||
const errorMessage = await response.text();
|
||||
this.ws.write('-- Tunnel Error Response --');
|
||||
this.ws.write(errorMessage);
|
||||
this.ws.write('-- End Response --');
|
||||
this.ngrokEmitter.emit('onTunnelError', {
|
||||
statusCode: response.status,
|
||||
errorMessage,
|
||||
});
|
||||
}
|
||||
this.ngrokEmitter.emit('onTunnelStatusPing', isErrorResponse ? TunnelStatus.Error : TunnelStatus.Active);
|
||||
}
|
||||
|
||||
public async disconnect(url?: string) {
|
||||
|
@ -108,6 +142,7 @@ export class NgrokInstance {
|
|||
delete this.tunnels[response.url];
|
||||
this.ngrokEmitter.emit('disconnect', response.url);
|
||||
});
|
||||
clearInterval(this.intervalForHealthCheck);
|
||||
}
|
||||
|
||||
public kill() {
|
||||
|
@ -120,10 +155,12 @@ export class NgrokInstance {
|
|||
this.ngrokProcess.kill();
|
||||
this.ngrokProcess = null;
|
||||
this.tunnels = {};
|
||||
this.cleanUpNgrokExpirationTimer();
|
||||
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 };
|
||||
}
|
||||
|
@ -157,25 +194,13 @@ export class NgrokInstance {
|
|||
return { inspectUrl: this.inspectUrl };
|
||||
}
|
||||
|
||||
/** Checks if the ngrok tunnel is due for expiration */
|
||||
private checkForNgrokExpiration(): void {
|
||||
const currentTime = Date.now();
|
||||
const timeElapsed = currentTime - this.ngrokStartTime;
|
||||
if (timeElapsed >= intervals.expirationTime) {
|
||||
this.cleanUpNgrokExpirationTimer();
|
||||
this.ngrokEmitter.emit('expired');
|
||||
} else {
|
||||
this.ngrokExpirationTimer = setTimeout(this.checkForNgrokExpiration.bind(this), intervals.expirationPoll);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/** Clears the ngrok expiration timer and resets the tunnel start time */
|
||||
private cleanUpNgrokExpirationTimer(): void {
|
||||
this.ngrokStartTime = null;
|
||||
clearTimeout(this.ngrokExpirationTimer);
|
||||
}
|
||||
|
||||
private async runTunnel(opts: NgrokOptions): Promise<{ url; inspectUrl }> {
|
||||
private async runTunnel(opts: NgrokOptions): Promise<{ url: string; inspectUrl: string }> {
|
||||
let retries = 100;
|
||||
const url = `${this.inspectUrl}/api/tunnels`;
|
||||
const body = JSON.stringify(opts);
|
||||
|
@ -208,10 +233,16 @@ export class NgrokInstance {
|
|||
if (opts.proto === 'http' && opts.bind_tls) {
|
||||
this.tunnels[publicUrl.replace('https', 'http')] = uri + ' (http)';
|
||||
}
|
||||
this.ngrokStartTime = Date.now();
|
||||
this.ngrokExpirationTimer = setTimeout(this.checkForNgrokExpiration.bind(this), intervals.expirationPoll);
|
||||
|
||||
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.checkTunnelStatus(publicUrl);
|
||||
this.pendingConnection = null;
|
||||
return { url: publicUrl, inspectUrl: this.inspectUrl };
|
||||
}
|
||||
|
@ -220,31 +251,40 @@ export class NgrokInstance {
|
|||
private spawnNgrok(opts: NgrokOptions): ChildProcess {
|
||||
const filename = `${opts.path ? path.basename(opts.path) : bin}`;
|
||||
const folder = opts.path ? path.dirname(opts.path) : path.join(__dirname, 'bin');
|
||||
const args = ['start', '--none', '--log=stdout', `--region=${opts.region}`];
|
||||
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.`
|
||||
);
|
||||
try {
|
||||
this.ws.write('Ngrok Logger starting');
|
||||
const args = ['start', '--none', `--log=stdout`, `--region=${opts.region}`];
|
||||
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;
|
||||
}
|
||||
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 = {};
|
||||
this.cleanUpNgrokExpirationTimer();
|
||||
this.ngrokEmitter.emit('disconnect');
|
||||
});
|
||||
|
||||
ngrok.on('close', () => {
|
||||
this.cleanUpNgrokExpirationTimer();
|
||||
this.ngrokEmitter.emit('close');
|
||||
});
|
||||
|
||||
ngrok.stderr.on('data', (data: Buffer) => this.ngrokEmitter.emit('error', data.toString()));
|
||||
return ngrok;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ import {
|
|||
isLocalHostUrl,
|
||||
LogItem,
|
||||
LogLevel,
|
||||
ngrokExpirationItem,
|
||||
textItem,
|
||||
} from '@bfemulator/sdk-shared';
|
||||
|
||||
|
@ -182,6 +181,10 @@ export class NgrokService {
|
|||
}
|
||||
}
|
||||
|
||||
public async pingTunnel(): Promise<void> {
|
||||
await this.ngrok.checkTunnelStatus(this.serviceUrl);
|
||||
}
|
||||
|
||||
public get ngrokEmitter(): EventEmitter {
|
||||
return this.ngrok.ngrokEmitter || undefined;
|
||||
}
|
||||
|
@ -190,11 +193,6 @@ export class NgrokService {
|
|||
return this.ngrok.running() || false;
|
||||
}
|
||||
|
||||
/** Logs a message in all active conversations that ngrok has expired */
|
||||
public broadcastNgrokExpired(): void {
|
||||
this.broadcast(ngrokExpirationItem('ngrok tunnel has expired.'));
|
||||
}
|
||||
|
||||
/** Logs messages signifying that ngrok has reconnected in all active conversations */
|
||||
public broadcastNgrokReconnected(): void {
|
||||
const bypassNgrokLocalhost = getSettings().framework.bypassNgrokLocalhost;
|
||||
|
@ -209,6 +207,12 @@ export class NgrokService {
|
|||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
|
|
@ -39,3 +39,4 @@ export * from './savedBotUrlsActions';
|
|||
export * from './updateActions';
|
||||
export * from './userActions';
|
||||
export * from './windowStateActions';
|
||||
export * from './ngrokTunnelActions';
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// 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,
|
||||
} 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(expectedStatus);
|
||||
expect(action.type).toBe(NgrokTunnelActions.setStatus);
|
||||
expect(action.payload.timestamp).toBe(new Date().toLocaleString());
|
||||
expect(action.payload.status).toBe(expectedStatus);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// 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/STATUS_CHECK',
|
||||
}
|
||||
|
||||
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: string;
|
||||
}
|
||||
|
||||
export interface NgrokTunnelAction<T> extends Action {
|
||||
type: NgrokTunnelActions;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export type NgrokTunnelPayloadTypes = TunnelError | TunnelInfo | TunnelStatusAndTimestamp;
|
||||
|
||||
export function updateNewTunnelInfo(payload: TunnelInfo): NgrokTunnelAction<TunnelInfo> {
|
||||
return {
|
||||
type: NgrokTunnelActions.setDetails,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTunnelStatus(tunnelStatus: TunnelStatus): NgrokTunnelAction<TunnelStatusAndTimestamp> {
|
||||
return {
|
||||
type: NgrokTunnelActions.setStatus,
|
||||
payload: {
|
||||
status: tunnelStatus,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTunnelError(payload: TunnelError): NgrokTunnelAction<TunnelError> {
|
||||
return {
|
||||
type: NgrokTunnelActions.updateOnError,
|
||||
payload,
|
||||
};
|
||||
}
|
|
@ -51,3 +51,4 @@ export * from './theme';
|
|||
export * from './update';
|
||||
export * from './users';
|
||||
export * from './windowState';
|
||||
export * from './ngrokTunnel';
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// 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,
|
||||
TunnelStatusAndTs,
|
||||
} 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,
|
||||
lastTunnelStatusCheckTS: '',
|
||||
};
|
||||
|
||||
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('Tunnel status should be set from payload', () => {
|
||||
const payload: TunnelStatusAndTs = {
|
||||
status: TunnelStatus.Active,
|
||||
ts: '12/27/2019, 1:30:00 PM',
|
||||
};
|
||||
const nextPayload: TunnelStatusAndTs = {
|
||||
status: TunnelStatus.Error,
|
||||
ts: '12/27/2019, 1:33:00 PM',
|
||||
};
|
||||
const actions: NgrokTunnelAction<NgrokTunnelPayloadTypes>[] = [
|
||||
{
|
||||
type: NgrokTunnelActions.setStatus,
|
||||
payload,
|
||||
},
|
||||
{
|
||||
type: NgrokTunnelActions.setStatus,
|
||||
payload: nextPayload,
|
||||
},
|
||||
];
|
||||
const startingState = { ...DEFAULT_STATE };
|
||||
const transientState = ngrokTunnel(startingState, actions[0]);
|
||||
expect(transientState.tunnelStatus).toBe(payload.status);
|
||||
|
||||
const finalState = ngrokTunnel(startingState, actions[1]);
|
||||
expect(finalState.tunnelStatus).toBe(nextPayload.status);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// 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,
|
||||
} from '../actions/ngrokTunnelActions';
|
||||
|
||||
export interface NgrokTunnelState {
|
||||
errors: TunnelError;
|
||||
publicUrl: string;
|
||||
inspectUrl: string;
|
||||
logPath: string;
|
||||
postmanCollectionPath: string;
|
||||
tunnelStatus: TunnelStatus;
|
||||
lastPingedTimestamp: string;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: NgrokTunnelState = {
|
||||
inspectUrl: 'http://127.0.0.1:4040',
|
||||
publicUrl: '',
|
||||
logPath: '',
|
||||
postmanCollectionPath: '',
|
||||
errors: {} as TunnelError,
|
||||
tunnelStatus: TunnelStatus.Inactive,
|
||||
lastPingedTimestamp: '',
|
||||
};
|
||||
|
||||
export const ngrokTunnel = (
|
||||
state: NgrokTunnelState = DEFAULT_STATE,
|
||||
action: NgrokTunnelAction<NgrokTunnelPayloadTypes>
|
||||
): NgrokTunnelState => {
|
||||
switch (action.type) {
|
||||
case NgrokTunnelActions.setDetails:
|
||||
state = {
|
||||
...state,
|
||||
...action.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,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return state;
|
||||
};
|
|
@ -81,6 +81,8 @@ import {
|
|||
ResourcesState,
|
||||
ThemeState,
|
||||
UpdateState,
|
||||
NgrokTunnelState,
|
||||
ngrokTunnel,
|
||||
} from './reducers';
|
||||
|
||||
export interface RootState {
|
||||
|
@ -101,6 +103,7 @@ export interface RootState {
|
|||
settings?: Settings;
|
||||
theme?: ThemeState;
|
||||
update?: UpdateState;
|
||||
ngrokTunnel?: NgrokTunnelState;
|
||||
}
|
||||
|
||||
export const DEFAULT_STATE = {
|
||||
|
@ -140,6 +143,7 @@ function initStore(): Store<RootState> {
|
|||
settings: settingsReducer,
|
||||
theme,
|
||||
update,
|
||||
ngrokTunnel,
|
||||
}),
|
||||
DEFAULT_STATE,
|
||||
applyMiddleware(forwardToRenderer, sagaMiddleware)
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// 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 { copyFileAsync } from './copyFileAsync';
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
copyFile: (sourcePath: string, destinationPath: string, cb: Function) => {
|
||||
if (!destinationPath || !sourcePath) {
|
||||
cb('Incorrect folder permissions.');
|
||||
}
|
||||
cb('');
|
||||
},
|
||||
}));
|
||||
|
||||
describe('copy files from source to destination asynchronously', () => {
|
||||
it('should resolve promise if operation was successful', async done => {
|
||||
await expect(copyFileAsync('1.txt', '2.txt')).resolves.toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should reject promise if unsuccessful', async (done: any) => {
|
||||
await expect(copyFileAsync('1.txt', '')).rejects.toBe('Error copying file: Incorrect folder permissions.');
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// 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 fs from 'fs';
|
||||
|
||||
export const copyFileAsync = (sourcePath: string, destinationPath: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.copyFile(sourcePath, destinationPath, err => {
|
||||
if (err) {
|
||||
reject(`Error copying file: ${err}`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
|
@ -48,3 +48,4 @@ export * from './showOpenDialog';
|
|||
export * from './showSaveDialog';
|
||||
export * from './writeFile';
|
||||
export * from './getThemes';
|
||||
export * from './writeStream';
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/* eslint-disable typescript/camelcase */
|
||||
//
|
||||
// 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: {},
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// 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 fs from 'fs';
|
||||
|
||||
import { writeStream as writeStreamAsync, FileWriteStream } from './writeStream';
|
||||
|
||||
// eslint-disable-next-line typescript/no-unused-vars
|
||||
let fileMock = '';
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
createWriteStream: () => ({
|
||||
write: content => {
|
||||
fileMock += content;
|
||||
},
|
||||
end: () => {
|
||||
fileMock = '';
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fileMock = '';
|
||||
});
|
||||
|
||||
describe('writing content file in chunks', () => {
|
||||
it('should write to file multiple chunks of content', () => {
|
||||
const pathToFile = '/Documents/BorFramework-Emulator/logger.txt';
|
||||
const createStreamSpy = jest.spyOn((fs as any).default, 'createWriteStream');
|
||||
const wsInstance: FileWriteStream = writeStreamAsync(pathToFile);
|
||||
const writeSpy = jest.spyOn(createStreamSpy.mock.results[0].value, 'write');
|
||||
wsInstance.write('test', false);
|
||||
wsInstance.write('test', false);
|
||||
wsInstance.write('test', true);
|
||||
wsInstance.end();
|
||||
expect(createStreamSpy).toHaveBeenCalledTimes(1);
|
||||
expect(writeSpy).toHaveBeenCalledTimes(3);
|
||||
expect(writeSpy).toHaveBeenLastCalledWith('test\n');
|
||||
expect(writeSpy).toHaveBeenNthCalledWith(1, 'test');
|
||||
createStreamSpy.mockClear();
|
||||
});
|
||||
|
||||
it('should call end on the stream', () => {
|
||||
const pathToFile = '/Documents/BorFramework-Emulator/logger.txt';
|
||||
const createStreamSpy = jest.spyOn((fs as any).default, 'createWriteStream');
|
||||
const wsInstance: FileWriteStream = writeStreamAsync(pathToFile);
|
||||
const endSpy = jest.spyOn(createStreamSpy.mock.results[0].value, 'end');
|
||||
wsInstance.write('test', false);
|
||||
wsInstance.write('test', false);
|
||||
wsInstance.write('test', true);
|
||||
wsInstance.end();
|
||||
expect(endSpy).toHaveBeenCalledTimes(1);
|
||||
createStreamSpy.mockClear();
|
||||
endSpy.mockClear();
|
||||
});
|
||||
|
||||
it('should call create stream with the correct path', () => {
|
||||
const pathToFile = '/Documents/BorFramework-Emulator/logger.txt';
|
||||
const createStreamSpy = jest.spyOn((fs as any).default, 'createWriteStream');
|
||||
writeStreamAsync(pathToFile);
|
||||
expect(createStreamSpy).toHaveBeenLastCalledWith(pathToFile);
|
||||
createStreamSpy.mockClear();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// 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 { createWriteStream } from 'fs';
|
||||
|
||||
export interface FileWriteStream {
|
||||
write: (content: string, newLine?: boolean) => void;
|
||||
end: () => void;
|
||||
}
|
||||
|
||||
export const writeStream = (pathToFile: string) => {
|
||||
const stream = createWriteStream(pathToFile);
|
||||
|
||||
const write = (content: string, newLine: boolean = true) => {
|
||||
stream.write(content + (newLine ? '\n' : ''));
|
||||
};
|
||||
|
||||
const end = () => {
|
||||
stream.end();
|
||||
};
|
||||
|
||||
return {
|
||||
write,
|
||||
end,
|
||||
};
|
||||
};
|
|
@ -95,6 +95,7 @@ export const SharedConstants = {
|
|||
UnlinkFile: 'shell:unlink-file',
|
||||
RenameFile: 'shell:rename-file',
|
||||
QuitAndInstall: 'shell:quit-and-install',
|
||||
CopyFile: 'shell:copy-file',
|
||||
},
|
||||
|
||||
Emulator: {
|
||||
|
@ -150,6 +151,8 @@ export const SharedConstants = {
|
|||
Ngrok: {
|
||||
Reconnect: 'ngrok:reconnect',
|
||||
KillProcess: 'ngrok:killProcess',
|
||||
PingTunnel: 'ngrok:pingTunnel',
|
||||
OpenStatusViewer: 'ngrok:openStatusViewer',
|
||||
},
|
||||
|
||||
Notifications: {
|
||||
|
@ -201,6 +204,7 @@ 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/master/content/CHANNELS.md',
|
||||
|
@ -211,6 +215,7 @@ 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: {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -13,8 +13,8 @@
|
|||
"@bfemulator/sdk-client": "^1.0.0",
|
||||
"botframework-schema": "^4.3.4",
|
||||
"deep-diff": "^1.0.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6",
|
||||
"react-json-tree": "^0.11.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -36,8 +36,8 @@
|
|||
"@types/deep-diff": "^1.0.0",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/lscache": "^1.0.29",
|
||||
"@types/react": "~16.3.2",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"@types/react": "16.9.17",
|
||||
"@types/react-dom": "16.9.4",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "24.8.0",
|
||||
"babel-preset-react-app": "^3.1.1",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -14,8 +14,8 @@
|
|||
"botframework-schema": "^4.3.4",
|
||||
"lscache": "^1.1.0",
|
||||
"luis-apis": "2.5.1",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --hot --inline --mode development --content-base ./public",
|
||||
|
@ -35,8 +35,8 @@
|
|||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/lscache": "^1.0.29",
|
||||
"@types/react": "~16.3.2",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"@types/react": "16.9.17",
|
||||
"@types/react-dom": "16.9.4",
|
||||
"babel-jest": "24.8.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -12,8 +12,8 @@
|
|||
"botframework-config": "4.4.0",
|
||||
"botframework-schema": "^4.3.4",
|
||||
"qnamaker": "^1.3.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
|
@ -35,8 +35,8 @@
|
|||
"@babel/runtime": "^7.1.5",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/node": "8.9.3",
|
||||
"@types/react": "~16.3.2",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"@types/react": "16.9.17",
|
||||
"@types/react-dom": "16.9.4",
|
||||
"babel-jest": "24.8.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"css-loader": "^1.0.1",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -26,8 +26,8 @@
|
|||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/react": "~16.3.2",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"@types/react": "16.9.17",
|
||||
"@types/react-dom": "16.9.4",
|
||||
"babel-jest": "24.8.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"enzyme": "^3.3.0",
|
||||
|
@ -56,8 +56,8 @@
|
|||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.5",
|
||||
"@bfemulator/app-shared": "^1.0.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6"
|
||||
},
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "../../../../testSetup.js",
|
||||
|
|
|
@ -221,7 +221,7 @@ export class Splitter extends React.Component<SplitterProps, SplitterState> {
|
|||
this.panes[index].ref = ReactDom.findDOMNode(element);
|
||||
};
|
||||
|
||||
public onActuatorMouseDown = (e: MouseEvent<HTMLDivElement>, splitterIndex: number): void => {
|
||||
private onActuatorMouseDown = (e: MouseEvent<HTMLDivElement>, splitterIndex: number): void => {
|
||||
clearSelection();
|
||||
// cache splitter dimensions
|
||||
this.splitters[splitterIndex].dimensions = this.splitters[splitterIndex].ref.getBoundingClientRect();
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -15,6 +15,7 @@
|
|||
"noImplicitReturns": true,
|
||||
"noImplicitThis": false,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
// "strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true,
|
||||
|
|
Загрузка…
Ссылка в новой задаче