Skip to content

Commit

Permalink
refactor(cardano-graphql-services): serialize tx submit errors in HTT…
Browse files Browse the repository at this point in the history
…P response

- Also sets the status code dynamically based on presence of tx submission errors.
- Note: This currently looses the custom type precision, however it appears there's
a feature in development to address this:
sindresorhus/serialize-error#48
- Tests are also improved to avoid false positives
  • Loading branch information
rhyslbw authored and mkazlauskas committed Mar 23, 2022
1 parent 9a09f91 commit 24b43b3
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 48 deletions.
1 change: 1 addition & 0 deletions packages/cardano-graphql-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"express": "^4.17.3",
"graphql-request": "npm:graphql-request-configurable-serializer@4.0.0",
"reflect-metadata": "~0.1.13",
"serialize-error": "^8",
"ts-log": "^2.2.4",
"type-graphql": "~1.1.1"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Cardano, TxSubmitProvider } from '@cardano-sdk/core';
import { HttpServer, ListenConfig } from '../Http';
import { Logger, dummyLogger } from 'ts-log';
import { TxSubmitProvider } from '@cardano-sdk/core';
import { serializeError } from 'serialize-error';
import express, { Router } from 'express';
const bodyParser = require('body-parser');

Expand All @@ -25,7 +26,6 @@ export class TxSubmitHttpServer extends HttpServer {
} catch (error) {
logger.error(error);
body = error.message;
// Todo: Inspect errors, and set code based on type of error.
res.statusCode = 500;
}
res.send(body);
Expand All @@ -41,10 +41,9 @@ export class TxSubmitHttpServer extends HttpServer {
await txSubmitProvider.submitTx(new Uint8Array(req.body));
body = undefined;
} catch (error) {
logger.error(error);
body = error.message;
// Todo: Inspect errors, and set code based on type of error.
res.statusCode = 500;
res.statusCode = Cardano.util.hasTxSubmissionError(error) ? 400 : 500;
body = Array.isArray(error) ? error.map((e) => serializeError(e)) : serializeError(error);
logger.error(body);
}
res.send(body);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Cardano, TxSubmitProvider } from '@cardano-sdk/core';
import { TxSubmitHttpServer } from '../../src';
import { TxSubmitProvider } from '@cardano-sdk/core';
import { getPort } from 'get-port-please';
import cbor from 'cbor';
import got from 'got';

const tx = cbor.encode('#####');
const BAD_REQUEST_STRING = 'Response code 400 (Bad Request)';

describe('TxSubmitHttpServer', () => {
let txSubmitProvider: TxSubmitProvider;
Expand All @@ -16,61 +17,103 @@ describe('TxSubmitHttpServer', () => {
port = await getPort();
apiUrlBase = `http://localhost:${port}`;
txSubmitProvider = { healthCheck: jest.fn(() => Promise.resolve({ ok: true })), submitTx: jest.fn() };
txSubmitHttpServer = TxSubmitHttpServer.create({ port }, { txSubmitProvider });
await txSubmitHttpServer.initialize();
await txSubmitHttpServer.start();
});

afterAll(async () => {
await txSubmitHttpServer.shutdown();
});

afterEach(async () => {
jest.resetAllMocks();
});

it('health', async () => {
const res = await got(`${apiUrlBase}/health`, {
// eslint-disable-next-line sonarjs/no-duplicate-string
headers: { 'Content-Type': 'application/json' }
describe('healthy and successful submission', () => {
beforeAll(async () => {
txSubmitHttpServer = TxSubmitHttpServer.create({ port }, { txSubmitProvider });
await txSubmitHttpServer.initialize();
await txSubmitHttpServer.start();
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body)).toEqual({ ok: true });
});

describe('submit', () => {
it('returns a 200 coded response with a well formed HTTP request', async () => {
const res = await got.post(`${apiUrlBase}/submit`, {
body: tx,
headers: { 'Content-Type': 'application/cbor' },
method: 'post'
});
expect(res.statusCode).toBe(200);
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(1);
afterAll(async () => {
await txSubmitHttpServer.shutdown();
});

it('returns a 400 coded response if the wrong content type header is used', async () => {
try {
await got.post(`${apiUrlBase}/submit`, {
body: tx,
describe('/health', () => {
it('forwards the txSubmitProvider health response', async () => {
const res = await got(`${apiUrlBase}/health`, {
// eslint-disable-next-line sonarjs/no-duplicate-string
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
expect(error.message).toBe('Response code 400 (Bad Request)');
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0);
}
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body)).toEqual({ ok: true });
});
});

it('returns a 400 coded response if the tx is not sent as binary data', async () => {
try {
await got.post(`${apiUrlBase}/submit`, {
body: Buffer.from(new Uint8Array()).toString(),
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
expect(error.message).toBe('Response code 400 (Bad Request)');
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0);
}
describe('/submit', () => {
it('returns a 200 coded response with a well formed HTTP request', async () => {
expect(
(
await got.post(`${apiUrlBase}/submit`, {
body: tx,
headers: { 'Content-Type': 'application/cbor' },
method: 'post'
})
).statusCode
).toEqual(200);
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(1);
});

it('returns a 400 coded response if the wrong content type header is used', async () => {
try {
await got.post(`${apiUrlBase}/submit`, {
body: tx,
headers: { 'Content-Type': 'application/json' }
});
throw new Error('fail');
} catch (error) {
expect(error.response.statusCode).toBe(400);
expect(error.message).toBe(BAD_REQUEST_STRING);
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0);
}
});

it('returns a 400 coded response if the tx is not sent as binary data', async () => {
try {
await got.post(`${apiUrlBase}/submit`, {
body: '',
headers: { 'Content-Type': 'application/json' }
});
throw new Error('fail');
} catch (error) {
expect(error.response.statusCode).toBe(400);
expect(error.message).toBe(BAD_REQUEST_STRING);
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0);
}
});
});
});

describe('healthy but failing submission', () => {
describe('/submit', () => {
// eslint-disable-next-line max-len
it('returns a 400 coded response with detail in the body to a transaction containing a domain violation', async () => {
const stubErrors = [new Cardano.TxSubmissionErrors.BadInputsError({ badInputs: [] })];
txSubmitProvider = {
healthCheck: jest.fn(() => Promise.resolve({ ok: true })),
submitTx: jest.fn(() => Promise.reject(stubErrors))
};
txSubmitHttpServer = TxSubmitHttpServer.create({ port }, { txSubmitProvider });
await txSubmitHttpServer.initialize();
await txSubmitHttpServer.start();
try {
await got.post(`${apiUrlBase}/submit`, {
body: Buffer.from(new Uint8Array()).toString(),
headers: { 'Content-Type': 'application/cbor' }
});
throw new Error('fail');
} catch (error) {
expect(error.response.statusCode).toBe(400);
expect(JSON.parse(error.response.body)[0].name).toEqual(stubErrors[0].name);
expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(1);
await txSubmitHttpServer.shutdown();
}
});
});
});
});
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8851,6 +8851,13 @@ serialize-error@^7.0.1:
dependencies:
type-fest "^0.13.1"

serialize-error@^8:
version "8.1.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67"
integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==
dependencies:
type-fest "^0.20.2"

serve-static@1.14.2:
version "1.14.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa"
Expand Down

0 comments on commit 24b43b3

Please sign in to comment.