diff --git a/spec/fakes/crash/crash-api-response.ts b/spec/fakes/crash/crash-api-response.ts index 43662a4..98f393f 100644 --- a/spec/fakes/crash/crash-api-response.ts +++ b/spec/fakes/crash/crash-api-response.ts @@ -1,7 +1,7 @@ import { GroupableThreadCollection, ProcessingStatus } from '@crash'; import { EventType } from '@events'; import { createFakeEvents } from '@spec/fakes/events/events'; -import { CrashDetailsRawResponse } from 'src/crash/crash-details/crash-details'; +import { CrashDetailsRawResponse, CrashStatus } from 'src/crash/crash-details/crash-details'; export const createFakeCrashApiResponse = () => ({ processed: ProcessingStatus.Complete as number, @@ -27,6 +27,7 @@ export const createFakeCrashApiResponse = () => ({ platform: 'NES', previousCrashId: 998, processor: 'Pentium 4', + status: CrashStatus.Open, stackKey: 'myConsoleCrasher(1337)', stackKeyComment: 'hello world!', stackKeyId: 117, diff --git a/src/crash/crash-api-client/crash-api-client.e2e.ts b/src/crash/crash-api-client/crash-api-client.e2e.ts index 36f9a45..685726a 100644 --- a/src/crash/crash-api-client/crash-api-client.e2e.ts +++ b/src/crash/crash-api-client/crash-api-client.e2e.ts @@ -1,28 +1,30 @@ import { BugSplatApiClient } from '@common'; -import { CrashApiClient } from '@crash'; +import { CrashApiClient, CrashStatus } from '@crash'; import { config } from '@spec/config'; -import { postNativeCrashAndSymbols } from '@spec/files/native/post-native-crash'; +import { postNativeCrashAndWaitForCrashToProcess } from '@spec/files/native/post-native-crash'; describe('CrashApiClient', () => { let crashClient: CrashApiClient; let application: string; let version: string; let id: number; + let stackKeyId: number; beforeEach(async () => { const { host, email, password } = config; const bugsplatApiClient = await BugSplatApiClient.createAuthenticatedClientForNode(email, password, host); application = 'myConsoleCrasher'; version = `${Math.random() * 1000000}`; - const result = await postNativeCrashAndSymbols( + crashClient = new CrashApiClient(bugsplatApiClient); + const result = await postNativeCrashAndWaitForCrashToProcess( bugsplatApiClient, + crashClient, config.database, application, version ); id = result.crashId; - - crashClient = new CrashApiClient(bugsplatApiClient); + stackKeyId = result.stackKeyId; }); describe('getCrashById', () => { @@ -39,7 +41,15 @@ describe('CrashApiClient', () => { it('should return 200 for a recent crash that has symbols', async () => { const response = await crashClient.reprocessCrash(config.database, id); - expect(response.success).toEqual(true); + expect(response.status).toEqual('success'); + }); + }); + + describe('postStatus', () => { + it('should return 200', async () => { + const response = await crashClient.postStatus(config.database, stackKeyId, CrashStatus.Open); + + expect(response.status).toEqual('success'); }); }); -}); \ No newline at end of file +}); diff --git a/src/crash/crash-api-client/crash-api-client.spec.ts b/src/crash/crash-api-client/crash-api-client.spec.ts index 08159b0..e65506a 100644 --- a/src/crash/crash-api-client/crash-api-client.spec.ts +++ b/src/crash/crash-api-client/crash-api-client.spec.ts @@ -3,7 +3,7 @@ import { createFakeBugSplatApiClient } from '@spec/fakes/common/bugsplat-api-cli import { createFakeFormData } from '@spec/fakes/common/form-data'; import { createFakeResponseBody } from '@spec/fakes/common/response'; import { createFakeCrashApiResponse } from '@spec/fakes/crash/crash-api-response'; -import { createCrashDetails } from '../crash-details/crash-details'; +import { CrashStatus, createCrashDetails } from '../crash-details/crash-details'; describe('CrashApiClient', () => { const database = 'fred'; @@ -170,4 +170,59 @@ describe('CrashApiClient', () => { } }); }); -}); \ No newline at end of file + + describe('postStatus', () => { + let client: CrashApiClient; + let fakePostStatusApiResponse; + let fakeBugSplatApiClient; + let result; + + beforeEach(async () => { + fakePostStatusApiResponse = { success: true }; + const fakeResponse = createFakeResponseBody(200, fakePostStatusApiResponse); + fakeBugSplatApiClient = createFakeBugSplatApiClient(fakeFormData, fakeResponse); + client = new CrashApiClient(fakeBugSplatApiClient); + + result = await client.postStatus(database, id, CrashStatus.Closed); + }); + + it('should call fetch with correct route', () => { + expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith('/api/crash/status', jasmine.anything()); + }); + + it('should call fetch with formData containing database, id and status', () => { + expect(fakeFormData.append).toHaveBeenCalledWith('database', database); + expect(fakeFormData.append).toHaveBeenCalledWith('groupId', id.toString()); + expect(fakeFormData.append).toHaveBeenCalledWith('status', CrashStatus.Closed.toString()); + }); + + it('should return response json', () => { + expect(result).toEqual(fakePostStatusApiResponse); + }); + + it('should throw if status is not 200', async () => { + const message = 'Bad request'; + + try { + const fakePostStatusErrorBody = { message }; + const fakeResponse = createFakeResponseBody(400, fakePostStatusErrorBody, false); + const fakeBugSplatApiClient = createFakeBugSplatApiClient(fakeFormData, fakeResponse); + const client = new CrashApiClient(fakeBugSplatApiClient); + + await client.postStatus(database, id, CrashStatus.Closed); + fail('postStatus was supposed to throw!'); + } catch (error: any) { + expect(error.message).toEqual(message); + } + }); + + it('should throw if database is falsy', async () => { + try { + await client.postStatus('', id, CrashStatus.Closed); + fail('postStatus was supposed to throw!'); + } catch (error: any) { + expect(error.message).toMatch(/to be a non white space string/); + } + }); + }); +}); diff --git a/src/crash/crash-api-client/crash-api-client.ts b/src/crash/crash-api-client/crash-api-client.ts index e24a0d6..121b6f9 100644 --- a/src/crash/crash-api-client/crash-api-client.ts +++ b/src/crash/crash-api-client/crash-api-client.ts @@ -1,7 +1,7 @@ import { ApiClient } from '@common'; import { CrashDetails } from '@crash'; import ac from 'argument-contracts'; -import { CrashDetailsRawResponse, createCrashDetails } from '../crash-details/crash-details'; +import { CrashDetailsRawResponse, CrashStatus, createCrashDetails } from '../crash-details/crash-details'; export class CrashApiClient { @@ -75,9 +75,38 @@ export class CrashApiClient { return json as SuccessResponse; } + + async postStatus(database: string, groupId: number, status: CrashStatus): Promise { + ac.assertNonWhiteSpaceString(database, 'database'); + ac.assertNumber(groupId, 'groupId'); + ac.assertNumber(status, 'status'); + + const formData = this._client.createFormData(); + formData.append('database', database); + formData.append('groupId', groupId.toString()); + formData.append('status', status.toString()); + const init = { + method: 'POST', + body: formData, + cache: 'no-cache', + credentials: 'include', + redirect: 'follow', + duplex: 'half' + } as RequestInit; + + const response = await this._client.fetch('/api/crash/status', init); + const json = await response.json(); + + if (response.status !== 200) { + throw new Error((json as ErrorResponse).message); + } + + return json as SuccessResponse; + } } -type SuccessResponse = { success: boolean }; +type SuccessResponse = { status: 'success' }; type ErrorResponse = { message: string }; type GetCrashByIdResponse = CrashDetailsRawResponse | ErrorResponse; type ReprocessCrashResponse = SuccessResponse | ErrorResponse; +type PostStatusResponse = SuccessResponse | ErrorResponse; \ No newline at end of file diff --git a/src/crash/crash-details/crash-details.spec.ts b/src/crash/crash-details/crash-details.spec.ts index 8e80dee..a855a8f 100644 --- a/src/crash/crash-details/crash-details.spec.ts +++ b/src/crash/crash-details/crash-details.spec.ts @@ -1,4 +1,4 @@ -import { AdditionalInfo, ProcessingStatus } from '@crash'; +import { AdditionalInfo, CrashStatus, ProcessingStatus } from '@crash'; import { createFakeCrashApiResponse } from '@spec/fakes/crash/crash-api-response'; import ac from 'argument-contracts'; import { createEvents } from '../../events/events-api-client/event'; @@ -44,7 +44,8 @@ describe('createCrashDetails', () => { processor: 'OBAN-10.0.7.144', comments: null, processed: ProcessingStatus.Complete, - thread: ({ stackFrames: [], stackKeyId: 0 }) + thread: ({ stackFrames: [], stackKeyId: 0 }), + status: CrashStatus.Open, }; const result = createCrashDetails(options); diff --git a/src/crash/crash-details/crash-details.ts b/src/crash/crash-details/crash-details.ts index 1f0e251..db8b340 100644 --- a/src/crash/crash-details/crash-details.ts +++ b/src/crash/crash-details/crash-details.ts @@ -24,6 +24,12 @@ export enum DefectTrackerType { Favro = 'Favro', } +export enum CrashStatus { + Open = 0, + Closed = 1, + Regression = 2, +} + export interface CrashDetails { processed: ProcessingStatus; @@ -50,6 +56,7 @@ export interface CrashDetails { platform: string; previousCrashId: number; processor: string; + status: CrashStatus; stackKey: string; stackKeyComment: string; stackKeyId: number; @@ -93,6 +100,7 @@ export function createCrashDetails(options: CrashDetailsRawResponse): CrashDetai ac.assertType(options.thread, ThreadCollection, 'options.thread'); ac.assertType(options.events, Array, 'options.events'); + const status = (options.status || 0) as CrashStatus; const events = createEvents(options.events as EventResponseObject[]); const thread = new GroupableThreadCollection({ ...options.thread, @@ -120,6 +128,7 @@ export function createCrashDetails(options: CrashDetailsRawResponse): CrashDetai ipAddress, platform, processor, + status, stackKey, stackKeyComment, stackKeyDefectLabel, diff --git a/src/crash/index.ts b/src/crash/index.ts index 8d5cc6c..cea61d5 100644 --- a/src/crash/index.ts +++ b/src/crash/index.ts @@ -1,6 +1,6 @@ export { AdditionalInfo } from './additional-info/additional-info'; export { CrashApiClient } from './crash-api-client/crash-api-client'; -export { CrashDetails, DefectTrackerType, ProcessingStatus } from './crash-details/crash-details'; +export { CrashDetails, DefectTrackerType, ProcessingStatus, CrashStatus } from './crash-details/crash-details'; export { Module } from './module/module'; export { Register } from './register/register'; export { StackFrame, VariableValuePair } from './stack-frame/stack-frame'; diff --git a/src/crashes/crashes-api-row/crashes-api-row.ts b/src/crashes/crashes-api-row/crashes-api-row.ts index ea75bbc..a117ddb 100644 --- a/src/crashes/crashes-api-row/crashes-api-row.ts +++ b/src/crashes/crashes-api-row/crashes-api-row.ts @@ -1,3 +1,4 @@ +import { CrashStatus } from '@crash'; import { safeParseJson } from '../../common/parse'; interface CrashData { @@ -14,6 +15,7 @@ interface CrashData { defectId: string; defectUrl: string; defectLabel: string; + status: CrashStatus; skDefectId: string; skDefectUrl: string; skDefectLabel: string; @@ -70,6 +72,7 @@ export enum CrashTypeId { export class CrashesApiRow implements CrashDataWithMappedProperties { id: number; groupByCount: number; + status: CrashStatus; stackKey: string; stackId: number; stackKeyId: number; @@ -97,6 +100,7 @@ export class CrashesApiRow implements CrashDataWithMappedProperties { constructor(rawApiRow: CrashesApiResponseRow) { this.id = Number(rawApiRow.id); this.groupByCount = Number(rawApiRow.groupByCount) || 0; + this.status = Number(rawApiRow.status) as CrashStatus; this.stackKey = rawApiRow.stackKey; this.stackKeyId = Number(rawApiRow.stackKeyId); this.stackId = Number(rawApiRow.stackId); diff --git a/src/summary/summary-api-client/summary-api-client.ts b/src/summary/summary-api-client/summary-api-client.ts index 6473c28..00fb665 100644 --- a/src/summary/summary-api-client/summary-api-client.ts +++ b/src/summary/summary-api-client/summary-api-client.ts @@ -33,7 +33,8 @@ export class SummaryApiClient { row.stackKeyDefectLabel, row.comments, Number(row.subKeyDepth), - Number(row.userSum) + Number(row.userSum), + Number(row.status) ) ); diff --git a/src/summary/summary-api-row/summary-api-row.ts b/src/summary/summary-api-row/summary-api-row.ts index dac16ee..61240bb 100644 --- a/src/summary/summary-api-row/summary-api-row.ts +++ b/src/summary/summary-api-row/summary-api-row.ts @@ -1,4 +1,7 @@ +import { CrashStatus } from '@crash'; + interface SummaryData { + status: CrashStatus, stackKey: string, firstReport: string, lastReport: string, @@ -36,6 +39,7 @@ export class SummaryApiRow implements SummaryDataWithMappedProperties { public stackKeyDefectLabel: string, public comments: string, public subKeyDepth: number, - public userSum: number + public userSum: number, + public status: CrashStatus ) { } }