Files
atom/packages/exception-reporting/spec/reporter-spec.js
2018-09-28 07:46:03 -07:00

480 lines
18 KiB
JavaScript

const Reporter = require('../lib/reporter')
const semver = require('semver')
const os = require('os')
const path = require('path')
const fs = require('fs-plus')
let osVersion = `${os.platform()}-${os.arch()}-${os.release()}`
let getReleaseChannel = version => {
return (version.indexOf('beta') > -1)
? 'beta'
: (version.indexOf('dev') > -1)
? 'dev'
: 'stable'
}
describe("Reporter", () => {
let reporter, requests, initialStackTraceLimit, initialFsGetHomeDirectory, mockActivePackages
beforeEach(() => {
reporter = new Reporter({
request: (url, options) => requests.push(Object.assign({url}, options)),
alwaysReport: true,
reportPreviousErrors: false
})
requests = []
mockActivePackages = []
spyOn(atom.packages, 'getActivePackages').andCallFake(() => mockActivePackages)
initialStackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 1
initialFsGetHomeDirectory = fs.getHomeDirectory
})
afterEach(() => {
fs.getHomeDirectory = initialFsGetHomeDirectory
Error.stackTraceLimit = initialStackTraceLimit
})
describe(".reportUncaughtException(error)", () => {
it("posts errors originated inside Atom Core to BugSnag", () => {
const repositoryRootPath = path.join(__dirname, '..')
reporter = new Reporter({
request: (url, options) => requests.push(Object.assign({url}, options)),
alwaysReport: true,
reportPreviousErrors: false,
resourcePath: repositoryRootPath
})
let error = new Error()
Error.captureStackTrace(error)
reporter.reportUncaughtException(error)
let [lineNumber, columnNumber] = error.stack.match(/.js:(\d+):(\d+)/).slice(1).map(s => parseInt(s))
expect(requests.length).toBe(1)
let [request] = requests
expect(request.method).toBe("POST")
expect(request.url).toBe("https://notify.bugsnag.com")
expect(request.headers.get("Content-Type")).toBe("application/json")
let body = JSON.parse(request.body)
// Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject
expect(body).toEqual({
"apiKey": Reporter.API_KEY,
"notifier": {
"name": "Atom",
"version": Reporter.LIB_VERSION,
"url": "https://www.atom.io"
},
"events": [
{
"payloadVersion": "2",
"exceptions": [
{
"errorClass": "Error",
"message": "",
"stacktrace": [
{
"method": semver.gt(process.versions.electron, '1.6.0') ? 'Spec.it' : 'it',
"file": "spec/reporter-spec.js",
"lineNumber": lineNumber,
"columnNumber": columnNumber
}
]
}
],
"severity": "error",
"user": {},
"app": {
"version": atom.getVersion(),
"releaseStage": getReleaseChannel(atom.getVersion())
},
"device": {
"osVersion": osVersion
}
}
]
});})
it("posts errors originated outside Atom Core to BugSnag", () => {
fs.getHomeDirectory = () => path.join(__dirname, '..', '..')
let error = new Error()
Error.captureStackTrace(error)
reporter.reportUncaughtException(error)
let [lineNumber, columnNumber] = error.stack.match(/.js:(\d+):(\d+)/).slice(1).map(s => parseInt(s))
expect(requests.length).toBe(1)
let [request] = requests
expect(request.method).toBe("POST")
expect(request.url).toBe("https://notify.bugsnag.com")
expect(request.headers.get("Content-Type")).toBe("application/json")
let body = JSON.parse(request.body)
// Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject
expect(body).toEqual({
"apiKey": Reporter.API_KEY,
"notifier": {
"name": "Atom",
"version": Reporter.LIB_VERSION,
"url": "https://www.atom.io"
},
"events": [
{
"payloadVersion": "2",
"exceptions": [
{
"errorClass": "Error",
"message": "",
"stacktrace": [
{
"method": semver.gt(process.versions.electron, '1.6.0') ? 'Spec.it' : 'it',
"file": '~/exception-reporting/spec/reporter-spec.js',
"lineNumber": lineNumber,
"columnNumber": columnNumber
}
]
}
],
"severity": "error",
"user": {},
"app": {
"version": atom.getVersion(),
"releaseStage": getReleaseChannel(atom.getVersion())
},
"device": {
"osVersion": osVersion
}
}
]
});})
describe("when the error object has `privateMetadata` and `privateMetadataDescription` fields", () => {
let [error, notification] = []
beforeEach(() => {
atom.notifications.clear()
spyOn(atom.notifications, 'addInfo').andCallThrough()
error = new Error()
Error.captureStackTrace(error)
error.metadata = {foo: "bar"}
error.privateMetadata = {baz: "quux"}
error.privateMetadataDescription = "The contents of baz"
})
it("posts a notification asking for consent", () => {
reporter.reportUncaughtException(error)
expect(atom.notifications.addInfo).toHaveBeenCalled()
})
it("submits the error with the private metadata if the user consents", () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough()
reporter.reportUncaughtException(error)
reporter.reportUncaughtException.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[1].text).toMatch(/Yes/)
notificationOptions.buttons[1].onDidClick()
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error)
expect(reporter.reportUncaughtException.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar", baz: "quux"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user does not consent", () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough()
reporter.reportUncaughtException(error)
reporter.reportUncaughtException.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[0].text).toMatch(/No/)
notificationOptions.buttons[0].onDidClick()
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error)
expect(reporter.reportUncaughtException.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user dismisses the notification", () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough()
reporter.reportUncaughtException(error)
reporter.reportUncaughtException.reset()
notification = atom.notifications.getNotifications()[0]
notification.dismiss()
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error)
expect(reporter.reportUncaughtException.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"});});})
it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
mockActivePackages = [
{name: 'user-1', path: '/Users/user/.atom/packages/user-1', metadata: {version: '1.0.0'}},
{name: 'user-2', path: '/Users/user/.atom/packages/user-2', metadata: {version: '1.2.0'}},
{name: 'bundled-1', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1', metadata: {version: '1.0.0'}},
{name: 'bundled-2', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', metadata: {version: '1.2.0'}},
]
const packageDirPaths = ['/Users/user/.atom/packages']
spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths)
let error = new Error()
Error.captureStackTrace(error)
reporter.reportUncaughtException(error)
expect(error.metadata.userPackages).toEqual({
'user-1': '1.0.0',
'user-2': '1.2.0'
})
expect(error.metadata.bundledPackages).toEqual({
'bundled-1': '1.0.0',
'bundled-2': '1.2.0'
})
})
it('adds previous error messages and assertion failures to the reported metadata', () => {
reporter.reportPreviousErrors = true
reporter.reportUncaughtException(new Error('A'))
reporter.reportUncaughtException(new Error('B'))
reporter.reportFailedAssertion(new Error('X'))
reporter.reportFailedAssertion(new Error('Y'))
reporter.reportUncaughtException(new Error('C'))
expect(requests.length).toBe(5)
const lastRequest = requests[requests.length - 1]
const body = JSON.parse(lastRequest.body)
console.log(body);
expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B'])
expect(body.events[0].metaData.previousAssertionFailures).toEqual(['X', 'Y'])
})
})
describe(".reportFailedAssertion(error)", () => {
it("posts warnings to bugsnag", () => {
fs.getHomeDirectory = () => path.join(__dirname, '..', '..')
let error = new Error()
Error.captureStackTrace(error)
reporter.reportFailedAssertion(error)
let [lineNumber, columnNumber] = error.stack.match(/.js:(\d+):(\d+)/).slice(1).map(s => parseInt(s))
expect(requests.length).toBe(1)
let [request] = requests
expect(request.method).toBe("POST")
expect(request.url).toBe("https://notify.bugsnag.com")
expect(request.headers.get("Content-Type")).toBe("application/json")
let body = JSON.parse(request.body)
// Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject
expect(body).toEqual({
"apiKey": Reporter.API_KEY,
"notifier": {
"name": "Atom",
"version": Reporter.LIB_VERSION,
"url": "https://www.atom.io"
},
"events": [
{
"payloadVersion": "2",
"exceptions": [
{
"errorClass": "Error",
"message": "",
"stacktrace": [
{
"method": semver.gt(process.versions.electron, '1.6.0') ? 'Spec.it' : 'it',
"file": '~/exception-reporting/spec/reporter-spec.js',
"lineNumber": lineNumber,
"columnNumber": columnNumber
}
]
}
],
"severity": "warning",
"user": {},
"app": {
"version": atom.getVersion(),
"releaseStage": getReleaseChannel(atom.getVersion())
},
"device": {
"osVersion": osVersion
}
}
]
});})
describe("when the error object has `privateMetadata` and `privateMetadataDescription` fields", () => {
let [error, notification] = []
beforeEach(() => {
atom.notifications.clear()
spyOn(atom.notifications, 'addInfo').andCallThrough()
error = new Error()
Error.captureStackTrace(error)
error.metadata = {foo: "bar"}
error.privateMetadata = {baz: "quux"}
error.privateMetadataDescription = "The contents of baz"
})
it("posts a notification asking for consent", () => {
reporter.reportFailedAssertion(error)
expect(atom.notifications.addInfo).toHaveBeenCalled()
})
it("submits the error with the private metadata if the user consents", () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough()
reporter.reportFailedAssertion(error)
reporter.reportFailedAssertion.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[1].text).toMatch(/Yes/)
notificationOptions.buttons[1].onDidClick()
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error)
expect(reporter.reportFailedAssertion.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar", baz: "quux"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user does not consent", () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough()
reporter.reportFailedAssertion(error)
reporter.reportFailedAssertion.reset()
notification = atom.notifications.getNotifications()[0]
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]
expect(notificationOptions.buttons[0].text).toMatch(/No/)
notificationOptions.buttons[0].onDidClick()
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error)
expect(reporter.reportFailedAssertion.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"})
expect(notification.isDismissed()).toBe(true)
})
it("submits the error without the private metadata if the user dismisses the notification", () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough()
reporter.reportFailedAssertion(error)
reporter.reportFailedAssertion.reset()
notification = atom.notifications.getNotifications()[0]
notification.dismiss()
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error)
expect(reporter.reportFailedAssertion.callCount).toBe(1)
expect(error.privateMetadata).toBeUndefined()
expect(error.privateMetadataDescription).toBeUndefined()
expect(error.metadata).toEqual({foo: "bar"})
})
it("only notifies the user once for a given 'privateMetadataRequestName'", () => {
let fakeStorage = {}
spyOn(global.localStorage, 'setItem').andCallFake((key, value) => fakeStorage[key] = value)
spyOn(global.localStorage, 'getItem').andCallFake(key => fakeStorage[key])
error.privateMetadataRequestName = 'foo'
reporter.reportFailedAssertion(error)
expect(atom.notifications.addInfo).toHaveBeenCalled()
atom.notifications.addInfo.reset()
reporter.reportFailedAssertion(error)
expect(atom.notifications.addInfo).not.toHaveBeenCalled()
let error2 = new Error()
Error.captureStackTrace(error2)
error2.privateMetadataDescription = 'Something about you'
error2.privateMetadata = {baz: 'quux'}
error2.privateMetadataRequestName = 'bar'
reporter.reportFailedAssertion(error2)
expect(atom.notifications.addInfo).toHaveBeenCalled()
})
})
it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
mockActivePackages = [
{name: 'user-1', path: '/Users/user/.atom/packages/user-1', metadata: {version: '1.0.0'}},
{name: 'user-2', path: '/Users/user/.atom/packages/user-2', metadata: {version: '1.2.0'}},
{name: 'bundled-1', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1', metadata: {version: '1.0.0'}},
{name: 'bundled-2', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', metadata: {version: '1.2.0'}},
]
const packageDirPaths = ['/Users/user/.atom/packages']
spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths)
let error = new Error()
Error.captureStackTrace(error)
reporter.reportFailedAssertion(error)
expect(error.metadata.userPackages).toEqual({
'user-1': '1.0.0',
'user-2': '1.2.0'
})
expect(error.metadata.bundledPackages).toEqual({
'bundled-1': '1.0.0',
'bundled-2': '1.2.0'
})
})
it('adds previous error messages and assertion failures to the reported metadata', () => {
reporter.reportPreviousErrors = true
reporter.reportUncaughtException(new Error('A'))
reporter.reportUncaughtException(new Error('B'))
reporter.reportFailedAssertion(new Error('X'))
reporter.reportFailedAssertion(new Error('Y'))
reporter.reportFailedAssertion(new Error('C'))
expect(requests.length).toBe(5)
const lastRequest = requests[requests.length - 1]
const body = JSON.parse(lastRequest.body)
expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B'])
expect(body.events[0].metaData.previousAssertionFailures).toEqual(['X', 'Y'])
})
})
})