@owlmeans/api 0.1.18-rc.4 → 0.1.18-rc.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/api/SKILL.md +96 -11
- package/build/consts.d.ts +7 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +7 -0
- package/build/consts.js.map +1 -1
- package/build/errors.d.ts +30 -2
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +46 -4
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +25 -8
- package/build/service.js.map +1 -1
- package/build/status/index.d.ts +31 -0
- package/build/status/index.d.ts.map +1 -0
- package/build/status/index.js +65 -0
- package/build/status/index.js.map +1 -0
- package/build/status/marker.d.ts +12 -0
- package/build/status/marker.d.ts.map +1 -0
- package/build/status/marker.js +27 -0
- package/build/status/marker.js.map +1 -0
- package/build/utils/handler.d.ts +7 -0
- package/build/utils/handler.d.ts.map +1 -1
- package/build/utils/handler.js +60 -26
- package/build/utils/handler.js.map +1 -1
- package/package.json +19 -10
- package/src/consts.ts +8 -0
- package/src/errors.ts +55 -4
- package/src/index.ts +4 -1
- package/src/service.ts +33 -8
- package/src/status/index.ts +85 -0
- package/src/status/marker.ts +37 -0
- package/src/utils/handler.ts +71 -25
- package/tests/handler.spec.ts +229 -0
- package/tests/tsconfig.json +11 -0
- package/tsconfig.json +1 -1
- package/build/.gitkeep +0 -0
package/src/utils/handler.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { AbstractResponse } from '@owlmeans/entrypoint'
|
|
2
2
|
import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
3
3
|
import type { AxiosResponse } from 'axios'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, INCIDENT_ID_HEADER, DENIAL_KIND_HEADER,
|
|
6
|
+
ACCESS_DENIED_KIND, OK, SERVER_ERROR, UNAUTHORIZED_ERROR,
|
|
7
|
+
} from '../consts.js'
|
|
5
8
|
import { ResilientError } from '@owlmeans/error'
|
|
6
|
-
import { ApiClientError, ServerAuthError, ServerCrashedError } from '../errors.js'
|
|
9
|
+
import { ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError } from '../errors.js'
|
|
10
|
+
import { isIncidentBody } from '../status/index.js'
|
|
11
|
+
import type { ResponseStatusCarrier } from '../status/index.js'
|
|
7
12
|
|
|
8
13
|
export const processResponse = (response: AxiosResponse, reply: AbstractResponse<any>) => {
|
|
9
14
|
switch (response.status) {
|
|
@@ -20,34 +25,75 @@ export const processResponse = (response: AxiosResponse, reply: AbstractResponse
|
|
|
20
25
|
default:
|
|
21
26
|
|
|
22
27
|
console.error(response.status, response.statusText)
|
|
23
|
-
console.error(response.config
|
|
28
|
+
console.error(response.config?.url, response.headers)
|
|
24
29
|
console.error(response.data)
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
if (typeof response.data === 'string') {
|
|
28
|
-
reply.reject(ResilientError.ensure(response.data, true))
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`)
|
|
33
|
-
}
|
|
34
|
-
switch (response.status) {
|
|
35
|
-
case SERVER_ERROR:
|
|
36
|
-
reply.reject(new ServerCrashedError())
|
|
37
|
-
return
|
|
38
|
-
case UNAUTHORIZED_ERROR:
|
|
39
|
-
reply.reject(new ServerAuthError())
|
|
40
|
-
return
|
|
41
|
-
case FORBIDDEN_ERROR:
|
|
42
|
-
reply.reject(new ApiClientError('forbidden'))
|
|
43
|
-
return
|
|
44
|
-
default:
|
|
45
|
-
reply.reject(new ApiClientError(`${response.status}`))
|
|
46
|
-
return
|
|
47
|
-
}
|
|
31
|
+
reply.reject(failureOf(response))
|
|
48
32
|
}
|
|
49
33
|
}
|
|
50
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The error a non-2xx answer rejects with — always carrying the HTTP status and, when the server
|
|
37
|
+
* sent one, its incident id.
|
|
38
|
+
*
|
|
39
|
+
* A development body is the marshaled `ResilientError`: it is rebuilt into its own class and
|
|
40
|
+
* stamped with `responseStatus`. Anything else — the bare incident id of a production body, a
|
|
41
|
+
* proxy's HTML, a framework's JSON — becomes the status error of {@link statusError}.
|
|
42
|
+
*/
|
|
43
|
+
const failureOf = (response: AxiosResponse): Error => {
|
|
44
|
+
const incidentId = headerOf(response.headers, INCIDENT_ID_HEADER)
|
|
45
|
+
?? (isIncidentBody(response.data) ? response.data.trim() : undefined)
|
|
46
|
+
const denialKind = response.status === FORBIDDEN_ERROR
|
|
47
|
+
&& headerOf(response.headers, DENIAL_KIND_HEADER) === ACCESS_DENIED_KIND
|
|
48
|
+
? ACCESS_DENIED_KIND : undefined
|
|
49
|
+
|
|
50
|
+
if (typeof response.data === 'string' && response.data.includes(ResilientError.separator)) {
|
|
51
|
+
try {
|
|
52
|
+
const error = ResilientError.ensure(response.data, true)
|
|
53
|
+
error.incidentId ??= incidentId
|
|
54
|
+
Object.assign(error, {
|
|
55
|
+
responseStatus: response.status, denialKind,
|
|
56
|
+
} satisfies ResponseStatusCarrier)
|
|
57
|
+
|
|
58
|
+
return error
|
|
59
|
+
} catch {
|
|
60
|
+
console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Object.assign(statusError(response.status, incidentId), { denialKind })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
|
|
69
|
+
* `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
|
|
70
|
+
* for every other one.
|
|
71
|
+
*/
|
|
72
|
+
export const statusError = (status: number, incidentId?: string): ApiClientError => {
|
|
73
|
+
const error = status === SERVER_ERROR ? new ServerCrashedError(incidentId)
|
|
74
|
+
: status === UNAUTHORIZED_ERROR ? new ServerAuthError(incidentId)
|
|
75
|
+
: status === FORBIDDEN_ERROR ? new ApiClientError(incidentId != null ? `forbidden:${incidentId}` : 'forbidden')
|
|
76
|
+
: new ApiStatusError(status, incidentId)
|
|
77
|
+
if (incidentId != null) {
|
|
78
|
+
error.incidentId = incidentId
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Object.assign(error, { responseStatus: status } satisfies ResponseStatusCarrier)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One response header, from an `AxiosHeaders` instance or a plain header object alike. */
|
|
85
|
+
const headerOf = (headers: unknown, name: string): string | undefined => {
|
|
86
|
+
if (headers == null || typeof headers !== 'object') {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
const get = (headers as { get?: unknown }).get
|
|
90
|
+
const viaGet = typeof get === 'function' ? get.call(headers, name) : undefined
|
|
91
|
+
const value = viaGet ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]
|
|
92
|
+
const first = Array.isArray(value) ? value[0] : value
|
|
93
|
+
|
|
94
|
+
return typeof first === 'string' && first.trim() !== '' ? first.trim() : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
51
97
|
const processEmptyResponse = (response: AxiosResponse, reply: AbstractResponse<any>, outcome: EntrypointOutcome) => {
|
|
52
98
|
if (response.data != null && response.data != '') {
|
|
53
99
|
reply.resolve(response.data, outcome)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AxiosHeaders } from 'axios'
|
|
3
|
+
import type { AxiosResponse } from 'axios'
|
|
4
|
+
import type { AbstractResponse } from '@owlmeans/entrypoint'
|
|
5
|
+
import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
6
|
+
import { ResilientError } from '@owlmeans/error'
|
|
7
|
+
import { RecordExists, UnknownRecordError } from '@owlmeans/resource'
|
|
8
|
+
import {
|
|
9
|
+
ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError, httpStatusOf, incidentIdOf,
|
|
10
|
+
INCIDENT_ID_HEADER,
|
|
11
|
+
DENIAL_KIND_HEADER, ACCESS_DENIED_KIND,
|
|
12
|
+
} from '../src/index.js'
|
|
13
|
+
import { processResponse, statusError } from '../src/utils/handler.js'
|
|
14
|
+
import * as status from '../src/status/index.js'
|
|
15
|
+
|
|
16
|
+
const INCIDENT = '6f1c1a52-8a0e-4f5e-9a7c-1f2d3e4c5b6a'
|
|
17
|
+
|
|
18
|
+
class ConsentRequired extends ResilientError {
|
|
19
|
+
public static override typeName = 'ApiSpecConsentRequired'
|
|
20
|
+
public static httpStatus = 428
|
|
21
|
+
|
|
22
|
+
constructor(message: string = 'error') {
|
|
23
|
+
super(ConsentRequired.typeName, `spec:consent-required:${message}`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
ResilientError.registerErrorClass(ConsentRequired)
|
|
27
|
+
|
|
28
|
+
class Unavailable extends ResilientError {
|
|
29
|
+
public static override typeName = 'ApiSpecUnavailable'
|
|
30
|
+
public static httpStatus = 503
|
|
31
|
+
|
|
32
|
+
constructor(message: string = 'error') {
|
|
33
|
+
super(Unavailable.typeName, message)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const respond = (
|
|
38
|
+
statusCode: number, data: unknown, headers: Record<string, string> | AxiosHeaders = {}
|
|
39
|
+
): AbstractResponse<unknown> => {
|
|
40
|
+
const reply: AbstractResponse<unknown> = {
|
|
41
|
+
resolve: (value, outcome) => { reply.value = value; reply.outcome = outcome },
|
|
42
|
+
reject: error => { reply.error = error },
|
|
43
|
+
}
|
|
44
|
+
processResponse({
|
|
45
|
+
status: statusCode, statusText: '', data, headers, config: { url: '/spec' },
|
|
46
|
+
} as unknown as AxiosResponse, reply)
|
|
47
|
+
|
|
48
|
+
return reply
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const quiet = console.error
|
|
52
|
+
beforeAll(() => { console.error = () => {} })
|
|
53
|
+
afterAll(() => { console.error = quiet })
|
|
54
|
+
|
|
55
|
+
describe('2xx', () => {
|
|
56
|
+
test('resolve exactly as before', () => {
|
|
57
|
+
expect(respond(200, { ok: true })).toMatchObject({ value: { ok: true }, outcome: EntrypointOutcome.Ok })
|
|
58
|
+
expect(respond(202, 'queued')).toMatchObject({ value: 'queued', outcome: EntrypointOutcome.Accepted })
|
|
59
|
+
expect(respond(204, '', { 'x-a': '1' })).toMatchObject({ value: { 'x-a': '1' }, outcome: EntrypointOutcome.Finished })
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('a production incident body keeps its status and incident id', () => {
|
|
64
|
+
test('only a labelled 403 is an auth/IAM denial', () => {
|
|
65
|
+
const denied = respond(403, INCIDENT, { [DENIAL_KIND_HEADER]: ACCESS_DENIED_KIND }).error
|
|
66
|
+
expect(status.isAccessDenied(denied)).toBe(true)
|
|
67
|
+
expect(status.isAccessDenied(respond(403, INCIDENT).error)).toBe(false)
|
|
68
|
+
expect(status.isAccessDenied(respond(401, INCIDENT, { [DENIAL_KIND_HEADER]: ACCESS_DENIED_KIND }).error)).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
test('428 with the header becomes ApiStatusError', () => {
|
|
71
|
+
const { error } = respond(428, INCIDENT, { [INCIDENT_ID_HEADER.toLowerCase()]: INCIDENT })
|
|
72
|
+
expect(error).toBeInstanceOf(ApiStatusError)
|
|
73
|
+
expect(error!.message).toBe(`api:client:status:428:${INCIDENT}`)
|
|
74
|
+
expect(httpStatusOf(error)).toBe(428)
|
|
75
|
+
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
76
|
+
expect((error as ApiStatusError).status).toBe(428)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('the header is read from AxiosHeaders too, and the body alone suffices', () => {
|
|
80
|
+
const headers = new AxiosHeaders({ [INCIDENT_ID_HEADER]: INCIDENT })
|
|
81
|
+
expect(incidentIdOf(respond(409, 'x', headers).error)).toBe(INCIDENT)
|
|
82
|
+
expect(incidentIdOf(respond(402, INCIDENT).error)).toBe(INCIDENT)
|
|
83
|
+
expect(httpStatusOf(respond(402, INCIDENT).error)).toBe(402)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('500, 401 and 403 keep their classes and markers', () => {
|
|
87
|
+
const crashed = respond(500, INCIDENT).error
|
|
88
|
+
expect(crashed).toBeInstanceOf(ServerCrashedError)
|
|
89
|
+
expect(crashed!.message).toBe(`api:client:crashed:${INCIDENT}`)
|
|
90
|
+
expect(httpStatusOf(crashed)).toBe(500)
|
|
91
|
+
|
|
92
|
+
const auth = respond(401, INCIDENT).error
|
|
93
|
+
expect(auth).toBeInstanceOf(ServerAuthError)
|
|
94
|
+
expect(auth!.message).toBe(`api:client:auth:${INCIDENT}`)
|
|
95
|
+
expect(httpStatusOf(auth)).toBe(401)
|
|
96
|
+
|
|
97
|
+
const forbidden = respond(403, INCIDENT).error
|
|
98
|
+
expect(forbidden).toBeInstanceOf(ApiClientError)
|
|
99
|
+
expect(forbidden).not.toBeInstanceOf(ApiStatusError)
|
|
100
|
+
expect(forbidden!.message).toBe(`api:client:forbidden:${INCIDENT}`)
|
|
101
|
+
expect(httpStatusOf(forbidden)).toBe(403)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('without an incident id the markers stay as before', () => {
|
|
105
|
+
expect(statusError(500).message).toBe('api:client:crashed:error')
|
|
106
|
+
expect(statusError(403).message).toBe('api:client:forbidden')
|
|
107
|
+
expect(incidentIdOf(statusError(500))).toBeNull()
|
|
108
|
+
expect(statusError(418).message).toBe('api:client:status:418')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('a proxy HTML page or framework JSON becomes a status error', () => {
|
|
112
|
+
const html = respond(502, '<html><body>Bad Gateway</body></html>').error
|
|
113
|
+
expect(html).toBeInstanceOf(ApiStatusError)
|
|
114
|
+
expect(httpStatusOf(html)).toBe(502)
|
|
115
|
+
expect(incidentIdOf(html)).toBeNull()
|
|
116
|
+
|
|
117
|
+
const json = respond(400, { statusCode: 400, error: 'Bad Request', message: 'body/x' }).error
|
|
118
|
+
expect(json).toBeInstanceOf(ApiStatusError)
|
|
119
|
+
expect(httpStatusOf(json)).toBe(400)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('no class declares a static httpStatus, so a rethrow upstream stays 500', () => {
|
|
123
|
+
for (const error of [statusError(428), statusError(500), statusError(401), statusError(403)]) {
|
|
124
|
+
expect((error.constructor as { httpStatus?: unknown }).httpStatus).toBeUndefined()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('a development body stays its typed class', () => {
|
|
130
|
+
test('rebuilt with responseStatus and incident id', () => {
|
|
131
|
+
const body = ResilientError.marshal(new ConsentRequired('2'), { includeStack: true, incidentId: INCIDENT }).message
|
|
132
|
+
const { error } = respond(428, body, { [INCIDENT_ID_HEADER]: INCIDENT })
|
|
133
|
+
expect(error).toBeInstanceOf(ConsentRequired)
|
|
134
|
+
expect(error!.message).toBe('spec:consent-required:2')
|
|
135
|
+
expect((error as { responseStatus?: number }).responseStatus).toBe(428)
|
|
136
|
+
expect(httpStatusOf(error)).toBe(428)
|
|
137
|
+
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('a typed api error keeps its marker fields', () => {
|
|
141
|
+
const body = ResilientError.marshal(new ApiStatusError(409, INCIDENT)).message
|
|
142
|
+
const error = respond(409, body).error as ApiStatusError
|
|
143
|
+
expect(error).toBeInstanceOf(ApiStatusError)
|
|
144
|
+
expect(error.status).toBe(409)
|
|
145
|
+
expect(error.incidentId).toBe(INCIDENT)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('a storage refusal (404 absent record, 409 taken key) rebuilds into its own class', () => {
|
|
149
|
+
for (const [refusal, code] of [
|
|
150
|
+
[new UnknownRecordError('shelf/3'), 404], [new RecordExists('isbn'), 409],
|
|
151
|
+
] as const) {
|
|
152
|
+
const body = ResilientError.marshal(refusal, { includeStack: true, incidentId: INCIDENT }).message
|
|
153
|
+
const { error } = respond(code, body, { [INCIDENT_ID_HEADER]: INCIDENT })
|
|
154
|
+
expect(error!.constructor).toBe(refusal.constructor)
|
|
155
|
+
expect(error!.message).toBe(refusal.message)
|
|
156
|
+
expect(httpStatusOf(error)).toBe(code)
|
|
157
|
+
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
158
|
+
}
|
|
159
|
+
const body = ResilientError.marshal(new UnknownRecordError('shelf/3')).message
|
|
160
|
+
expect((respond(404, body).error as UnknownRecordError).id).toBe('shelf/3')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('a class this process never registered still answers the status the server sent', () => {
|
|
164
|
+
const body = `ApiSpecUnregisteredRefusal${ResilientError.separator}spec:unregistered:x`
|
|
165
|
+
const { error } = respond(404, body, { [INCIDENT_ID_HEADER]: INCIDENT })
|
|
166
|
+
expect(error).not.toBeInstanceOf(ServerCrashedError)
|
|
167
|
+
expect(httpStatusOf(error)).toBe(404)
|
|
168
|
+
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('a production body of a storage refusal keeps the declared status', () => {
|
|
173
|
+
test('404 and 409 become status errors, not a crash', () => {
|
|
174
|
+
for (const code of [404, 409]) {
|
|
175
|
+
const { error } = respond(code, INCIDENT)
|
|
176
|
+
expect(error).toBeInstanceOf(ApiStatusError)
|
|
177
|
+
expect(error).not.toBeInstanceOf(ServerCrashedError)
|
|
178
|
+
expect(error!.message).toBe(`api:client:status:${code}:${INCIDENT}`)
|
|
179
|
+
expect(httpStatusOf(error)).toBe(code)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('marshal round trips', () => {
|
|
185
|
+
const roundTrip = (error: Error): ResilientError => ResilientError.ensure(ResilientError.marshal(error))
|
|
186
|
+
|
|
187
|
+
test('keep status and incident id of every client error', () => {
|
|
188
|
+
const cases: Array<[ApiClientError, number]> = [
|
|
189
|
+
[new ApiStatusError(428, INCIDENT), 428],
|
|
190
|
+
[new ServerCrashedError(INCIDENT), 500],
|
|
191
|
+
[new ServerAuthError(INCIDENT), 401],
|
|
192
|
+
[new ApiClientError(`forbidden:${INCIDENT}`), 403],
|
|
193
|
+
]
|
|
194
|
+
for (const [error, code] of cases) {
|
|
195
|
+
const back = roundTrip(error)
|
|
196
|
+
expect(back.constructor).toBe(error.constructor)
|
|
197
|
+
expect((back as ApiClientError).status).toBe(code)
|
|
198
|
+
expect(httpStatusOf(back)).toBe(code)
|
|
199
|
+
expect(incidentIdOf(back)).toBe(INCIDENT)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('a legacy bare-status marker still reads', () => {
|
|
204
|
+
expect(httpStatusOf(new ApiClientError('404'))).toBe(404)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('./status', () => {
|
|
209
|
+
test('a declared static 428 answers without any marker; an undeclared 5xx does not', () => {
|
|
210
|
+
expect(status.httpStatusOf(new ConsentRequired())).toBe(428)
|
|
211
|
+
expect(status.httpStatusOf(new Unavailable())).toBeNull()
|
|
212
|
+
expect(status.httpStatusOf(new Error('plain'))).toBeNull()
|
|
213
|
+
expect(status.httpStatusOf(null)).toBeNull()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('reads a marker from a plain error message or a type', () => {
|
|
217
|
+
expect(status.httpStatusOf(new Error(`${status.API_STATUS_MARKER}428:${INCIDENT}`))).toBe(428)
|
|
218
|
+
expect(status.httpStatusOf({ type: 'api:client:auth:error' })).toBe(401)
|
|
219
|
+
expect(status.incidentIdOf(new Error(`api:client:crashed:${INCIDENT}`))).toBe(INCIDENT)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('isIncidentBody accepts a bare incident UUID only', () => {
|
|
223
|
+
expect(status.isIncidentBody(INCIDENT)).toBe(true)
|
|
224
|
+
expect(status.isIncidentBody(`${INCIDENT}\n`)).toBe(true)
|
|
225
|
+
expect(status.isIncidentBody('Unauthorized')).toBe(false)
|
|
226
|
+
expect(status.isIncidentBody(`ApiError|||api:x|||${INCIDENT}`)).toBe(false)
|
|
227
|
+
expect(status.isIncidentBody({ id: INCIDENT })).toBe(false)
|
|
228
|
+
})
|
|
229
|
+
})
|
package/tsconfig.json
CHANGED
package/build/.gitkeep
DELETED
|
File without changes
|