Leverage shared AzureActionHandler (#150)

This commit is contained in:
Eric Jizba 2018-01-04 15:06:57 -08:00 коммит произвёл GitHub
Родитель 62842d7fc0
Коммит bf2b67589c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 43 добавлений и 215 удалений

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

@ -384,7 +384,7 @@
},
"dependencies": {
"azure-arm-cosmosdb": "^1.0.0-preview",
"azure-arm-resource": "^2.0.0-preview",
"azure-arm-resource": "^3.0.0-preview",
"azure-arm-storage": "^3.1.0",
"azure-arm-website": "^1.0.0-preview",
"clipboardy": "^1.2.2",
@ -392,9 +392,9 @@
"ms-rest": "^2.2.2",
"ms-rest-azure": "^2.3.1",
"request-promise": "^4.2.2",
"vscode-azureappservice": "~0.8.1",
"vscode-azureextensionui": "~0.3.1",
"vscode-extension-telemetry": "^0.0.6",
"vscode-azureappservice": "~0.8.2",
"vscode-azureextensionui": "~0.5.0",
"vscode-extension-telemetry": "^0.0.10",
"vscode-nls": "^2.0.2",
"vscode-azurekudu": "~0.1.4",
"xml2js": "^0.4.19"

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

@ -1,37 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from './localize';
export class ErrorData {
public readonly message: string;
public readonly errorType: string;
// tslint:disable-next-line:no-any
constructor(error: any) {
if (error instanceof Error) {
try {
// Azure errors have a JSON object in the message
// tslint:disable-next-line:no-unsafe-any
this.errorType = JSON.parse(error.message).Code;
// tslint:disable-next-line:no-unsafe-any
this.message = JSON.parse(error.message).Message;
} catch (err) {
this.errorType = error.constructor.name;
this.message = error.message;
}
} else if (typeof (error) === 'object' && error !== null) {
this.errorType = (<object>error).constructor.name;
this.message = JSON.stringify(error);
// tslint:disable-next-line:no-unsafe-any
} else if (error !== undefined && error !== null && error.toString && error.toString().trim() !== '') {
this.errorType = typeof (error);
// tslint:disable-next-line:no-unsafe-any
this.message = error.toString();
} else {
this.errorType = typeof (error);
this.message = localize('azFunc.unknownError', 'Unknown Error');
}
}
}

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

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ErrorData } from "./ErrorData";
import { parseError } from 'vscode-azureextensionui';
import { localize } from "./localize";
interface IFunctionJson {
@ -45,28 +45,28 @@ export class FunctionConfig {
// tslint:disable-next-line:no-any
public constructor(data: any) {
let parseError: string | undefined;
let errMessage: string | undefined;
try {
if (data === null || data === undefined) {
parseError = localize('noDataError', 'No data was supplied.');
errMessage = localize('noDataError', 'No data was supplied.');
} else {
// tslint:disable-next-line:no-unsafe-any
this.disabled = data.disabled === true;
// tslint:disable-next-line:no-unsafe-any
if (!data.bindings || !(data.bindings instanceof Array)) {
parseError = localize('expectedBindings', 'Expected "bindings" element of type "Array".');
errMessage = localize('expectedBindings', 'Expected "bindings" element of type "Array".');
} else {
this.functionJson = <IFunctionJson>data;
const inBinding: IFunctionBinding | undefined = this.functionJson.bindings.find((b: IFunctionBinding) => b.direction === BindingDirection.in);
if (inBinding === undefined) {
parseError = localize('noInBindingError', 'Expected a binding with direction "in".');
errMessage = localize('noInBindingError', 'Expected a binding with direction "in".');
} else {
this.inBinding = inBinding;
if (!inBinding.type) {
parseError = localize('inBindingTypeError', 'The binding with direction "in" must have a type.');
errMessage = localize('inBindingTypeError', 'The binding with direction "in" must have a type.');
} else {
this.inBindingType = inBinding.type;
if (inBinding.type.toLowerCase() === 'httptrigger') {
@ -74,7 +74,7 @@ export class FunctionConfig {
if (inBinding.authLevel) {
const authLevel: HttpAuthLevel | undefined = <HttpAuthLevel>HttpAuthLevel[inBinding.authLevel.toLowerCase()];
if (authLevel === undefined) {
parseError = localize('unrecognizedAuthLevel', 'Unrecognized auth level "{0}".', inBinding.authLevel);
errMessage = localize('unrecognizedAuthLevel', 'Unrecognized auth level "{0}".', inBinding.authLevel);
} else {
this.authLevel = authLevel;
}
@ -85,11 +85,11 @@ export class FunctionConfig {
}
}
} catch (error) {
parseError = (new ErrorData(error)).message;
errMessage = (parseError(error)).message;
}
if (parseError !== undefined) {
throw new Error(localize('functionJsonParseError', 'Failed to parse function.json: {0}', parseError));
if (errMessage !== undefined) {
throw new Error(localize('functionJsonParseError', 'Failed to parse function.json: {0}', errMessage));
}
}
}

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

@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { AzureTreeDataProvider, IAzureNode } from 'vscode-azureextensionui';
import { AzureTreeDataProvider, IAzureNode, TelemetryProperties } from 'vscode-azureextensionui';
import { FunctionAppTreeItem } from '../tree/FunctionAppTreeItem';
import { nodeUtils } from '../utils/nodeUtils';
export async function configureDeploymentSource(telemetryProperties: { [key: string]: string; }, tree: AzureTreeDataProvider, outputChannel: vscode.OutputChannel, node?: IAzureNode<FunctionAppTreeItem>): Promise<void> {
export async function configureDeploymentSource(telemetryProperties: TelemetryProperties, tree: AzureTreeDataProvider, outputChannel: vscode.OutputChannel, node?: IAzureNode<FunctionAppTreeItem>): Promise<void> {
if (!node) {
node = <IAzureNode<FunctionAppTreeItem>>await tree.showNodePicker(FunctionAppTreeItem.contextValue);
}

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

@ -6,7 +6,7 @@
import * as fse from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import { UserCancelledError } from 'vscode-azureextensionui';
import { TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui';
import { AzureAccount } from '../../azure-account.api';
import { DialogResponses } from '../../DialogResponses';
import { IUserInterface, Pick, PickWithData } from '../../IUserInterface';
@ -30,7 +30,7 @@ const requiredFunctionAppFiles: string[] = [
path.join('.vscode', 'launch.json') // NOTE: tasks.json is not required if the user prefers to run 'func host start' from the command line
];
async function validateIsFunctionApp(telemetryProperties: { [key: string]: string; }, outputChannel: vscode.OutputChannel, functionAppPath: string, ui: IUserInterface): Promise<void> {
async function validateIsFunctionApp(telemetryProperties: TelemetryProperties, outputChannel: vscode.OutputChannel, functionAppPath: string, ui: IUserInterface): Promise<void> {
if (requiredFunctionAppFiles.find((file: string) => !fse.existsSync(path.join(functionAppPath, file))) !== undefined) {
const message: string = localize('azFunc.notFunctionApp', 'The selected folder is not a function app project. Initialize Project?');
const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(message, DialogResponses.yes, DialogResponses.skipForNow, DialogResponses.cancel);
@ -78,7 +78,7 @@ async function promptForStringSetting(ui: IUserInterface, setting: ConfigSetting
}
export async function createFunction(
telemetryProperties: { [key: string]: string; },
telemetryProperties: TelemetryProperties,
outputChannel: vscode.OutputChannel,
azureAccount: AzureAccount,
templateData: TemplateData,

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

@ -7,6 +7,7 @@ import * as fse from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import { OutputChannel } from 'vscode';
import { TelemetryProperties } from 'vscode-azureextensionui';
import { IUserInterface, Pick } from '../../IUserInterface';
import { localize } from '../../localize';
import { extensionPrefix, ProjectLanguage, projectLanguageSetting, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../../ProjectSettings';
@ -38,7 +39,7 @@ const funcProblemMatcher: {} = {
}
};
export async function createNewProject(telemetryProperties: { [key: string]: string; }, outputChannel: OutputChannel, functionAppPath?: string, openFolder: boolean = true, ui: IUserInterface = new VSCodeUI()): Promise<void> {
export async function createNewProject(telemetryProperties: TelemetryProperties, outputChannel: OutputChannel, functionAppPath?: string, openFolder: boolean = true, ui: IUserInterface = new VSCodeUI()): Promise<void> {
if (functionAppPath === undefined) {
functionAppPath = await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
}

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

@ -11,7 +11,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { MessageItem } from 'vscode';
import { SiteWrapper } from 'vscode-azureappservice';
import { AzureTreeDataProvider, IAzureNode, IAzureParentNode, UserCancelledError } from 'vscode-azureextensionui';
import { AzureTreeDataProvider, IAzureNode, IAzureParentNode, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui';
import * as xml2js from 'xml2js';
import { DialogResponses } from '../DialogResponses';
import { ArgumentError } from '../errors';
@ -28,7 +28,7 @@ import { nodeUtils } from '../utils/nodeUtils';
import * as workspaceUtil from '../utils/workspace';
import { VSCodeUI } from '../VSCodeUI';
export async function deploy(telemetryProperties: { [key: string]: string; }, tree: AzureTreeDataProvider, outputChannel: vscode.OutputChannel, context?: IAzureParentNode<FunctionAppTreeItem> | vscode.Uri, ui: IUserInterface = new VSCodeUI()): Promise<void> {
export async function deploy(telemetryProperties: TelemetryProperties, tree: AzureTreeDataProvider, outputChannel: vscode.OutputChannel, context?: IAzureParentNode<FunctionAppTreeItem> | vscode.Uri, ui: IUserInterface = new VSCodeUI()): Promise<void> {
const uri: vscode.Uri | undefined = context && context instanceof vscode.Uri ? context : undefined;
let node: IAzureParentNode<FunctionAppTreeItem> | undefined = context && !(context instanceof vscode.Uri) ? context : undefined;

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

@ -7,7 +7,7 @@
import * as vscode from 'vscode';
import { AppSettingsTreeItem, AppSettingTreeItem } from 'vscode-azureappservice';
import { AzureTreeDataProvider, IAzureNode, IAzureParentNode, UserCancelledError } from 'vscode-azureextensionui';
import { AzureActionHandler, AzureTreeDataProvider, IAzureNode, IAzureParentNode, TelemetryMeasurements, TelemetryProperties } from 'vscode-azureextensionui';
import TelemetryReporter from 'vscode-extension-telemetry';
import { AzureAccount } from './azure-account.api';
import { configureDeploymentSource } from './commands/configureDeploymentSource';
@ -23,7 +23,6 @@ import { renameAppSetting } from './commands/renameAppSetting';
import { restartFunctionApp } from './commands/restartFunctionApp';
import { startFunctionApp } from './commands/startFunctionApp';
import { stopFunctionApp } from './commands/stopFunctionApp';
import { ErrorData } from './ErrorData';
import { localize } from './localize';
import { TemplateData } from './templates/TemplateData';
import { FunctionAppProvider } from './tree/FunctionAppProvider';
@ -55,24 +54,25 @@ export function activate(context: vscode.ExtensionContext): void {
const templateData: TemplateData = new TemplateData(context.globalState);
initCommand<IAzureNode>(context, outputChannel, 'azureFunctions.refresh', (node?: IAzureNode) => tree.refresh(node));
initAsyncCommand<IAzureNode>(context, outputChannel, 'azureFunctions.loadMore', async (node: IAzureNode) => await tree.loadMore(node));
initCommand<IAzureNode<FunctionAppTreeItem>>(context, outputChannel, 'azureFunctions.openInPortal', async (node?: IAzureNode<FunctionAppTreeItem>) => await openInPortal(tree, node));
initAsyncCommandWithCustomTelemetry(context, outputChannel, 'azureFunctions.createFunction', async (telemetryProperties: { [key: string]: string; }) => await createFunction(telemetryProperties, outputChannel, azureAccount, templateData));
initAsyncCommandWithCustomTelemetry(context, outputChannel, 'azureFunctions.createNewProject', async (telemetryProperties: { [key: string]: string; }) => await createNewProject(telemetryProperties, outputChannel));
initAsyncCommand<IAzureParentNode>(context, outputChannel, 'azureFunctions.createFunctionApp', async (node?: IAzureParentNode) => await createChildNode(tree, AzureTreeDataProvider.subscriptionContextValue, node));
initAsyncCommand<IAzureNode<FunctionAppTreeItem>>(context, outputChannel, 'azureFunctions.startFunctionApp', async (node?: IAzureNode<FunctionAppTreeItem>) => await startFunctionApp(tree, node));
initAsyncCommand<IAzureNode<FunctionAppTreeItem>>(context, outputChannel, 'azureFunctions.stopFunctionApp', async (node?: IAzureNode<FunctionAppTreeItem>) => await stopFunctionApp(tree, node));
initAsyncCommand<IAzureNode<FunctionAppTreeItem>>(context, outputChannel, 'azureFunctions.restartFunctionApp', async (node?: IAzureNode<FunctionAppTreeItem>) => await restartFunctionApp(tree, node));
initAsyncCommand<IAzureParentNode>(context, outputChannel, 'azureFunctions.deleteFunctionApp', async (node?: IAzureParentNode) => await deleteNode(tree, FunctionAppTreeItem.contextValue, node));
initAsyncCommandWithCustomTelemetry<IAzureParentNode<FunctionAppTreeItem> | vscode.Uri>(context, outputChannel, 'azureFunctions.deploy', async (telemetryProperties: { [key: string]: string; }, arg?: IAzureParentNode<FunctionAppTreeItem> | vscode.Uri) => await deploy(telemetryProperties, tree, outputChannel, arg));
initAsyncCommandWithCustomTelemetry<IAzureNode<FunctionAppTreeItem>>(context, outputChannel, 'azureFunctions.configureDeploymentSource', async (telemetryProperties: { [key: string]: string; }, node?: IAzureNode<FunctionAppTreeItem>) => await configureDeploymentSource(telemetryProperties, tree, outputChannel, node));
initAsyncCommand<IAzureNode<FunctionTreeItem>>(context, outputChannel, 'azureFunctions.copyFunctionUrl', async (node?: IAzureNode<FunctionTreeItem>) => await copyFunctionUrl(tree, node));
initAsyncCommand<IAzureNode>(context, outputChannel, 'azureFunctions.deleteFunction', async (node?: IAzureNode) => await deleteNode(tree, FunctionTreeItem.contextValue, node));
initAsyncCommand<IAzureParentNode>(context, outputChannel, 'azureFunctions.appSettings.add', async (node: IAzureParentNode) => await createChildNode(tree, AppSettingsTreeItem.contextValue, node));
initAsyncCommand<IAzureNode<AppSettingTreeItem>>(context, outputChannel, 'azureFunctions.appSettings.edit', async (node: IAzureNode<AppSettingTreeItem>) => await editAppSetting(tree, node));
initAsyncCommand<IAzureNode<AppSettingTreeItem>>(context, outputChannel, 'azureFunctions.appSettings.rename', async (node: IAzureNode<AppSettingTreeItem>) => await renameAppSetting(tree, node));
initAsyncCommand<IAzureNode<AppSettingTreeItem>>(context, outputChannel, 'azureFunctions.appSettings.delete', async (node: IAzureNode<AppSettingTreeItem>) => await deleteNode(tree, AppSettingTreeItem.contextValue, node));
const actionHandler: AzureActionHandler = new AzureActionHandler(context, outputChannel, reporter);
actionHandler.registerCommand('azureFunctions.refresh', (node?: IAzureNode) => tree.refresh(node));
actionHandler.registerCommand('azureFunctions.loadMore', async (node: IAzureNode) => await tree.loadMore(node));
actionHandler.registerCommand('azureFunctions.openInPortal', async (node?: IAzureNode<FunctionAppTreeItem>) => await openInPortal(tree, node));
actionHandler.registerCommandWithCustomTelemetry('azureFunctions.createFunction', async (properties: TelemetryProperties, _measurements: TelemetryMeasurements) => await createFunction(properties, outputChannel, azureAccount, templateData));
actionHandler.registerCommandWithCustomTelemetry('azureFunctions.createNewProject', async (properties: TelemetryProperties, _measurements: TelemetryMeasurements) => await createNewProject(properties, outputChannel));
actionHandler.registerCommand('azureFunctions.createFunctionApp', async (node?: IAzureParentNode) => await createChildNode(tree, AzureTreeDataProvider.subscriptionContextValue, node));
actionHandler.registerCommand('azureFunctions.startFunctionApp', async (node?: IAzureNode<FunctionAppTreeItem>) => await startFunctionApp(tree, node));
actionHandler.registerCommand('azureFunctions.stopFunctionApp', async (node?: IAzureNode<FunctionAppTreeItem>) => await stopFunctionApp(tree, node));
actionHandler.registerCommand('azureFunctions.restartFunctionApp', async (node?: IAzureNode<FunctionAppTreeItem>) => await restartFunctionApp(tree, node));
actionHandler.registerCommand('azureFunctions.deleteFunctionApp', async (node?: IAzureParentNode) => await deleteNode(tree, FunctionAppTreeItem.contextValue, node));
actionHandler.registerCommandWithCustomTelemetry('azureFunctions.deploy', async (properties: TelemetryProperties, _measurements: TelemetryMeasurements, arg?: IAzureParentNode<FunctionAppTreeItem> | vscode.Uri) => await deploy(properties, tree, outputChannel, arg));
actionHandler.registerCommandWithCustomTelemetry('azureFunctions.configureDeploymentSource', async (properties: TelemetryProperties, _measurements: TelemetryMeasurements, node?: IAzureNode<FunctionAppTreeItem>) => await configureDeploymentSource(properties, tree, outputChannel, node));
actionHandler.registerCommand('azureFunctions.copyFunctionUrl', async (node?: IAzureNode<FunctionTreeItem>) => await copyFunctionUrl(tree, node));
actionHandler.registerCommand('azureFunctions.deleteFunction', async (node?: IAzureNode) => await deleteNode(tree, FunctionTreeItem.contextValue, node));
actionHandler.registerCommand('azureFunctions.appSettings.add', async (node: IAzureParentNode) => await createChildNode(tree, AppSettingsTreeItem.contextValue, node));
actionHandler.registerCommand('azureFunctions.appSettings.edit', async (node: IAzureNode<AppSettingTreeItem>) => await editAppSetting(tree, node));
actionHandler.registerCommand('azureFunctions.appSettings.rename', async (node: IAzureNode<AppSettingTreeItem>) => await renameAppSetting(tree, node));
actionHandler.registerCommand('azureFunctions.appSettings.delete', async (node: IAzureNode<AppSettingTreeItem>) => await deleteNode(tree, AppSettingTreeItem.contextValue, node));
}
}
@ -80,56 +80,6 @@ export function activate(context: vscode.ExtensionContext): void {
export function deactivate(): void {
}
function initCommand<T>(extensionContext: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, commandId: string, callback: (context?: T) => void): void {
initAsyncCommand(extensionContext, outputChannel, commandId, async (context?: T) => callback(context));
}
function initAsyncCommand<T>(extensionContext: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, commandId: string, callback: (context?: T) => Promise<void>): void {
initAsyncCommandWithCustomTelemetry(extensionContext, outputChannel, commandId, async (_telemetryProperties: { [key: string]: string; }, context?: T) => await callback(context));
}
function initAsyncCommandWithCustomTelemetry<T>(extensionContext: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, commandId: string, callback: (telemetryProperties: { [key: string]: string; }, context?: T) => Promise<void>): void {
extensionContext.subscriptions.push(vscode.commands.registerCommand(commandId, async (...args: {}[]) => {
const start: number = Date.now();
let errorData: ErrorData | undefined;
const properties: { [key: string]: string; } = {};
properties.result = 'Succeeded';
try {
if (args.length === 0) {
await callback(properties);
} else {
await callback(properties, <T>args[0]);
}
} catch (error) {
if (error instanceof UserCancelledError) {
properties.result = 'Canceled';
} else {
properties.result = 'Failed';
errorData = new ErrorData(error);
// Always append the error to the output channel, but only 'show' the output channel for multiline errors
outputChannel.appendLine(localize('azFunc.Error', 'Error: {0}', errorData.message));
if (errorData.message.includes('\n')) {
outputChannel.show();
vscode.window.showErrorMessage(localize('azFunc.multilineError', 'An error has occured in the Azure Functions extension. Check output window for more details.'));
} else {
vscode.window.showErrorMessage(errorData.message);
}
}
} finally {
const end: number = Date.now();
if (errorData) {
properties.error = errorData.errorType;
properties.errorMessage = errorData.message;
}
if (reporter) {
reporter.sendTelemetryEvent(commandId, properties, { duration: (end - start) / 1000 });
}
}
}));
}
interface IPackageInfo {
name: string;
version: string;

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

@ -1,86 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ErrorData } from '../src/ErrorData';
suite('Error Data Tests', () => {
test('Generic Error', () => {
const ed: ErrorData = new ErrorData(new Error('test'));
assert.equal(ed.errorType, 'Error');
assert.equal(ed.message, 'test');
});
test('Specific Error', () => {
const ed: ErrorData = new ErrorData(new ReferenceError('test'));
assert.equal(ed.errorType, 'ReferenceError');
assert.equal(ed.message, 'test');
});
test('Azure Error', () => {
const ed: ErrorData = new ErrorData(new Error('{ "Code": "Conflict", "Message": "test" }'));
assert.equal(ed.errorType, 'Conflict');
assert.equal(ed.message, 'test');
});
test('String', () => {
const ed: ErrorData = new ErrorData('test');
assert.equal(ed.errorType, 'string');
assert.equal(ed.message, 'test');
});
test('Empty String', () => {
const ed: ErrorData = new ErrorData(' ');
assert.equal(ed.errorType, 'string');
assert.equal(ed.message, 'Unknown Error');
});
test('Object', () => {
const ed: ErrorData = new ErrorData({ errorCode: 1 });
assert.equal(ed.errorType, 'Object');
assert.equal(ed.message, '{"errorCode":1}');
});
test('Custom Object', () => {
class MyObject {
public readonly msg: string;
constructor(msg: string) { this.msg = msg; }
}
const ed: ErrorData = new ErrorData(new MyObject('test'));
assert.equal(ed.errorType, 'MyObject');
assert.equal(ed.message, '{"msg":"test"}');
});
test('Null', () => {
const ed: ErrorData = new ErrorData(null);
assert.equal(ed.errorType, 'object');
assert.equal(ed.message, 'Unknown Error');
});
test('Array', () => {
const ed: ErrorData = new ErrorData([1, 2]);
assert.equal(ed.errorType, 'Array');
assert.equal(ed.message, '[1,2]');
});
test('Number', () => {
const ed: ErrorData = new ErrorData(3);
assert.equal(ed.errorType, 'number');
assert.equal(ed.message, '3');
});
test('Boolean', () => {
const ed: ErrorData = new ErrorData(false);
assert.equal(ed.errorType, 'boolean');
assert.equal(ed.message, 'false');
});
test('Undefined', () => {
const ed: ErrorData = new ErrorData(undefined);
assert.equal(ed.errorType, 'undefined');
assert.equal(ed.message, 'Unknown Error');
});
});