Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "A simple CSV parser - BountyPay workflow demo",
"main": "src/parser.js",
"scripts": {
"test": "node test/parser.test.js"
"test": "node test/parser.test.js && node test/api-response.test.js"
},
"license": "MIT"
}
65 changes: 65 additions & 0 deletions src/api-response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const DEFAULT_EMPTY_MESSAGE = 'No data was returned by the server. Please try again.';
const DEFAULT_NETWORK_MESSAGE = 'No response was received from the server. Please try again.';
const DEFAULT_PARSE_MESSAGE = 'The server returned an invalid response. Please try again.';

function friendlyError(message, details = {}) {
return {
ok: false,
data: null,
error: {
message,
...details,
},
};
}

async function readBody(response) {
if (typeof response === 'string') {
return response;
}

if (response && typeof response.text === 'function') {
return response.text();
}

if (response && 'body' in response) {
return response.body;
}

return response;
}

async function parseApiResponse(response) {
if (!response) {
return friendlyError(DEFAULT_NETWORK_MESSAGE);
}

const status = typeof response.status === 'number' ? response.status : undefined;
const ok = typeof response.ok === 'boolean' ? response.ok : status === undefined || (status >= 200 && status < 300);

if (status === 204 || status === 205) {
return friendlyError(DEFAULT_EMPTY_MESSAGE, { status });
}

const body = await readBody(response);

if (body == null || (typeof body === 'string' && body.trim() === '')) {
return friendlyError(DEFAULT_EMPTY_MESSAGE, { status });
}

if (!ok) {
return friendlyError(`Request failed${status ? ` with status ${status}` : ''}. Please try again.`, { status });
}

if (typeof body !== 'string') {
return { ok: true, data: body, error: null };
}

try {
return { ok: true, data: JSON.parse(body), error: null };
} catch (_error) {
return friendlyError(DEFAULT_PARSE_MESSAGE, { status });
}
}

module.exports = { parseApiResponse };
76 changes: 76 additions & 0 deletions test/api-response.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const { parseApiResponse } = require('../src/api-response');

let passed = 0;
let failed = 0;

function assert(name, actual, expected) {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
if (a === e) {
console.log(` OK ${name}`);
passed++;
} else {
console.log(` FAIL ${name}`);
console.log(` Expected: ${e}`);
console.log(` Actual: ${a}`);
failed++;
}
}

async function run() {
console.log('\nAPI Response Tests\n');

assert(
'returns parsed JSON for a valid response',
await parseApiResponse({ ok: true, status: 200, text: async () => '{"items":[1,2]}' }),
{ ok: true, data: { items: [1, 2] }, error: null }
);

assert(
'handles null response without throwing',
await parseApiResponse(null),
{
ok: false,
data: null,
error: { message: 'No response was received from the server. Please try again.' },
}
);

assert(
'handles empty response body without throwing',
await parseApiResponse({ ok: true, status: 200, text: async () => ' ' }),
{
ok: false,
data: null,
error: { message: 'No data was returned by the server. Please try again.', status: 200 },
}
);

assert(
'handles 204 responses as empty data',
await parseApiResponse({ ok: true, status: 204, text: async () => '' }),
{
ok: false,
data: null,
error: { message: 'No data was returned by the server. Please try again.', status: 204 },
}
);

assert(
'returns a friendly parse error for malformed JSON',
await parseApiResponse({ ok: true, status: 200, text: async () => '{bad json' }),
{
ok: false,
data: null,
error: {
message: 'The server returned an invalid response. Please try again.',
status: 200,
},
}
);

console.log(`\nResults: ${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);
}

run();