Skip to content

Commit 2396b4a

Browse files
authored
[DEVREL-102] Handling multiple validation errors
2 parents 8e8e99f + 4748f48 commit 2396b4a

File tree

5 files changed

+184
-66
lines changed

5 files changed

+184
-66
lines changed

lib/Client.ts

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Configuration } from "./Configuration";
2-
import { Recipient } from "./Recipient";
32
import * as crypto from "crypto";
43
import * as request from "request";
5-
import { Exceptions } from './exceptions';
4+
import { Errors } from './errors';
5+
import ValidationError = Errors.ValidationError;
66

77
/**
88
* Private function to handle URL requests and standard responses
@@ -21,49 +21,49 @@ function sendRequest<T>(options: request.UriOptions) {
2121
// tslint:disable-next-line:cyclomatic-complexity
2222
request(options, (error: any, response: request.RequestResponse, responseBody: any) => {
2323
if (error) {
24-
reject(new Exceptions.ServerError(String(error)));
24+
reject(new Errors.ServerError(String(error)));
2525
} else {
2626
try {
2727
const data = JSON.parse(responseBody);
2828

2929
if (response.statusCode === 200) {
3030
resolve(data as T);
31-
32-
return;
31+
} else {
32+
const validationErrors: ValidationError[] = data.errors.map((e: any) => {
33+
return {
34+
code: e.code,
35+
field: e.field,
36+
message: e.message,
37+
};
38+
});
39+
40+
switch (response.statusCode) {
41+
case 400:
42+
reject(new Errors.MalformedError(validationErrors));
43+
break;
44+
case 401:
45+
reject(new Errors.AuthenticationError(validationErrors));
46+
break;
47+
case 403:
48+
reject(new Errors.AuthorizationError(validationErrors));
49+
break;
50+
case 404:
51+
reject(new Errors.NotFoundError(validationErrors));
52+
break;
53+
case 500:
54+
reject(new Errors.ServerError("Server error", validationErrors));
55+
break;
56+
case 503:
57+
reject(new Errors.DownForMaintenanceError(validationErrors));
58+
break;
59+
default:
60+
reject(new Errors.UnexpectedError(`Unexpected HTTP_RESPONSE #${response.statusCode}`, validationErrors));
61+
}
3362
}
3463

35-
const firstErr = (data.errors && Array.isArray(data.errors) && data.errors.length !== 0) ? data.errors[0] : undefined;
36-
switch (response.statusCode) {
37-
case 400:
38-
const message = firstErr.code === "invalid_field" ? `${firstErr.message}: ${firstErr.field}` : firstErr.message;
39-
reject(new Exceptions.Malformed(message || "Not Found"));
40-
41-
return;
42-
case 401:
43-
reject(new Exceptions.Authentication(firstErr.message || "Not Found"));
44-
45-
return;
46-
case 403:
47-
reject(new Exceptions.Authorization(firstErr.message || "Not Found"));
48-
49-
return;
50-
case 404:
51-
reject(new Exceptions.Authorization(firstErr.message || "Not Found"));
52-
53-
return;
54-
case 500:
55-
reject(new Exceptions.ServerError(firstErr.message || "Not Found"));
56-
57-
return;
58-
case 503:
59-
reject(new Exceptions.DownForMaintenance(firstErr.message || "Not Found"));
60-
61-
return;
62-
default:
63-
reject(new Exceptions.Unexpected(`Unexpected HTTP_RESPONSE #${response.statusCode}`));
64-
}
64+
return;
6565
} catch (err) {
66-
reject(new Exceptions.Unexpected(String(err)));
66+
reject(new Errors.UnexpectedError(String(err)));
6767
}
6868
}
6969
});

lib/errors/index.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// tslint:disable:max-classes-per-file
2+
3+
/**
4+
* @module exceptions
5+
*/
6+
export module Errors {
7+
export interface ValidationError {
8+
code: string;
9+
field: string;
10+
message: string;
11+
}
12+
13+
abstract class BaseError extends Error {
14+
public validationErrors?: ValidationError[];
15+
16+
protected constructor(message: string, validationErrors?: ValidationError[]) {
17+
super(message);
18+
this.validationErrors = validationErrors;
19+
}
20+
}
21+
22+
export class DownForMaintenanceError extends BaseError {
23+
public constructor(validationErrors?: ValidationError[]) {
24+
super("Down for maintenance", validationErrors);
25+
}
26+
}
27+
28+
export class ServerError extends BaseError {
29+
public constructor(message: string = "Server error", validationErrors?: ValidationError[]) {
30+
super(message, validationErrors);
31+
}
32+
}
33+
34+
export class UnexpectedError extends BaseError {
35+
public constructor(message: string, validationErrors?: ValidationError[]) {
36+
super(message, validationErrors);
37+
}
38+
}
39+
40+
export class NotFoundError extends BaseError {
41+
public constructor(validationErrors?: ValidationError[]) {
42+
super("Not Found", validationErrors);
43+
}
44+
}
45+
46+
export class AuthenticationError extends BaseError {
47+
public constructor(validationErrors?: ValidationError[]) {
48+
super("Authentication failed", validationErrors);
49+
}
50+
}
51+
52+
export class AuthorizationError extends BaseError {
53+
public constructor(validationErrors?: ValidationError[]) {
54+
super("Authorization failed", validationErrors);
55+
}
56+
}
57+
58+
export class MalformedError extends BaseError {
59+
public constructor(validationErrors?: ValidationError[]) {
60+
super("Malformed request", validationErrors);
61+
}
62+
}
63+
}

lib/exceptions/index.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

test/integration/PaymentSpec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,22 @@ describe('Payment', () => {
129129
assert.strictEqual(paymentsCollection[0].constructor, Payment);
130130
assert.strictEqual(payment.externalId, paymentsCollection[0].externalId);
131131
});
132+
133+
it('handles multiple validation errors', async () => {
134+
const nockDone = await startNockRec('payment-validation-errors.json');
135+
136+
try {
137+
await testingApiClient.payment.create('undefined', undefined);
138+
} catch (error) {
139+
nockDone();
140+
141+
assert.strictEqual(error.validationErrors.length, 2);
142+
assert.strictEqual(error.validationErrors[0].code, 'invalid_field');
143+
assert.strictEqual(error.validationErrors[0].field, 'batchId');
144+
assert.strictEqual(error.validationErrors[0].message, 'Value is invalid');
145+
assert.strictEqual(error.validationErrors[1].code, 'invalid_field');
146+
assert.strictEqual(error.validationErrors[1].field, 'recipient');
147+
assert.strictEqual(error.validationErrors[1].message, 'Value is invalid');
148+
}
149+
});
132150
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
[
2+
{
3+
"scope": "https://api.trolley.com:443",
4+
"method": "POST",
5+
"path": "/v1/batches/undefined/payments",
6+
"body": {},
7+
"status": 400,
8+
"response": {
9+
"ok": false,
10+
"errors": [
11+
{
12+
"code": "invalid_field",
13+
"message": "Value is invalid",
14+
"field": "batchId"
15+
},
16+
{
17+
"code": "invalid_field",
18+
"message": "Value is invalid",
19+
"field": "recipient"
20+
}
21+
]
22+
},
23+
"rawHeaders": [
24+
"Date",
25+
"Thu, 30 Mar 2023 01:38:35 GMT",
26+
"Content-Type",
27+
"application/json; charset=utf-8",
28+
"Content-Length",
29+
"169",
30+
"Connection",
31+
"close",
32+
"Cache-Control",
33+
"no-store, no-cache",
34+
"Content-Security-Policy",
35+
"default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
36+
"X-DNS-Prefetch-Control",
37+
"off",
38+
"Expect-CT",
39+
"max-age=0",
40+
"Strict-Transport-Security",
41+
"max-age=15552000; includeSubDomains",
42+
"X-Download-Options",
43+
"noopen",
44+
"X-Content-Type-Options",
45+
"nosniff",
46+
"X-Permitted-Cross-Domain-Policies",
47+
"none",
48+
"Referrer-Policy",
49+
"no-referrer",
50+
"X-XSS-Protection",
51+
"0",
52+
"Access-Control-Allow-Origin",
53+
"*",
54+
"X-Rate-Limit-Limit",
55+
"600",
56+
"X-Rate-Limit-Remaining",
57+
"599",
58+
"X-Rate-Limit-Reset",
59+
"1680140375",
60+
"Vary",
61+
"Origin",
62+
"ETag",
63+
"W/\"a9-yaEk6UF4nZNKcky6pKAYPn+yzQs\""
64+
],
65+
"responseIsBinary": false
66+
}
67+
]

0 commit comments

Comments
 (0)