Skip to content

Commit 5a23adf

Browse files
authored
Merge pull request #10 from Hyperproof/sync-20260109-140759
2 parents 66e5c90 + 36c25ac commit 5a23adf

24 files changed

Lines changed: 3600 additions & 714 deletions

.eslintrc.js

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,19 @@
1+
/* eslint-env node */
2+
13
module.exports = {
2-
env: {
3-
browser: false,
4-
commonjs: true,
5-
es2021: true,
6-
node: true
7-
},
8-
extends: [
9-
'eslint:recommended',
10-
'plugin:@typescript-eslint/recommended',
11-
'prettier'
12-
],
13-
parserOptions: {
14-
ecmaVersion: 'latest'
15-
},
16-
plugins: ['@typescript-eslint'],
17-
rules: {
18-
'@typescript-eslint/no-non-null-assertion': 0,
19-
'@typescript-eslint/ban-types': 0,
20-
'@typescript-eslint/explicit-module-boundary-types': ['off'],
21-
'@typescript-eslint/no-explicit-any': ['off']
22-
},
234
overrides: [
245
{
25-
files: ['*.js'],
6+
files: ['*.ts'],
7+
parser: '@typescript-eslint/parser',
8+
parserOptions: {
9+
ecmaVersion: 'latest',
10+
project: `${__dirname}/tsconfig.json`
11+
},
2612
rules: {
27-
'@typescript-eslint/no-var-requires': 'off'
13+
'max-lines-per-function': ['error', { max: 660, skipBlankLines: true }],
14+
'max-depth': ['error', 6],
15+
complexity: ['error', 20]
2816
}
2917
}
30-
],
31-
ignorePatterns: ['**/build/*']
18+
]
3219
};

.github/workflows/build.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '22'
20+
cache: 'yarn'
21+
22+
- name: Install dependencies
23+
run: yarn install --frozen-lockfile
24+
25+
- name: Build
26+
run: yarn build

package.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hyperproof/integration-sdk",
3-
"version": "1.2.0",
3+
"version": "6.0.0-beta",
44
"description": "Hyperproof Integration SDK",
55
"license": "MIT",
66
"repository": {
@@ -11,20 +11,22 @@
1111
"types": "lib/index.d.ts",
1212
"scripts": {
1313
"build": "tsc && copyfiles -u 1 \"src/**/*.d.ts\" lib",
14-
"lint": "./node_modules/eslint/bin/eslint.js src/**/*.ts"
14+
"lint": "eslint 'src/**/*.{js,ts}' --max-warnings 0",
15+
"test": "jest --config=../../packages/jest-config/lib/jest.config.js"
1516
},
1617
"engines": {
17-
"node": "^16.19.1 || ^18.17.1"
18+
"node": "^22.0.0"
1819
},
1920
"dependencies": {
21+
"@hyperproof/hypersync-models": "6.0.0-beta",
2022
"@js-joda/core": "3.2.0",
2123
"@pollyjs/adapter-node-http": "6.0.6",
2224
"@pollyjs/core": "6.0.6",
2325
"@pollyjs/persister-fs": "6.0.6",
2426
"abort-controller": "3.0.0",
2527
"body-parser": "1.20.3",
26-
"express": "4.21.0",
27-
"form-data": "3.0.0",
28+
"express": "4.21.2",
29+
"form-data": "3.0.4",
2830
"html-entities": "2.5.2",
2931
"http-errors": "2.0.0",
3032
"http-status-codes": "2.3.0",
@@ -34,18 +36,24 @@
3436
"node-fetch": "2.7.0",
3537
"query-string": "7.1.3",
3638
"superagent": "10.1.0",
39+
"uuid": "10.0.0",
3740
"xss": "1.0.15"
3841
},
3942
"devDependencies": {
4043
"@types/express": "^4.17.21",
41-
"@types/node-fetch": "^2.6.11",
44+
"@types/jest": "^29.5.4",
45+
"@types/node-fetch": "^2.6.13",
4246
"@types/superagent": "^8.1.9",
47+
"@types/uuid": "^8.3.1",
4348
"@typescript-eslint/eslint-plugin": "8.7.0",
4449
"@typescript-eslint/parser": "8.7.0",
4550
"copyfiles": "^2.4.1",
46-
"eslint": "8.57.1",
4751
"eslint-config-prettier": "^8.5.0",
52+
"eslint": "8.57.1",
53+
"jest-junit": "^12.2.0",
54+
"jest": "^29.6.4",
4855
"prettier": "^2.6.1",
56+
"ts-jest": "^29.1.1",
4957
"ts-node": "8.0.2",
5058
"typescript": "5.5.4"
5159
},

src/ApiClient.test.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Import node-fetch module to spy on it
2+
import * as nodeFetch from 'node-fetch';
3+
import { ApiClient } from './ApiClient';
4+
import { HttpMethod } from './models';
5+
6+
/* eslint-disable max-lines-per-function */
7+
import { StatusCodes } from 'http-status-codes';
8+
// Import after mock
9+
import { Response } from 'node-fetch';
10+
11+
jest.mock('./hyperproof-api/Logger');
12+
13+
// Mock only the default export (fetch function)
14+
const mockFetch = jest.spyOn(nodeFetch, 'default') as jest.MockedFunction<
15+
typeof nodeFetch.default
16+
>;
17+
18+
class TestApiClient extends ApiClient {
19+
// Expose protected methods for testing
20+
public async testParseResponseBodyJson(response: Response, url: string) {
21+
return this.parseResponseBodyJson(response, url);
22+
}
23+
24+
public async testGetStatusCodeFromErrorMessage(error?: any) {
25+
return this.getStatusCodeFromErrorMessage(error);
26+
}
27+
28+
public async testHandleNetworkError(err: any) {
29+
return this.handleNetworkError(err);
30+
}
31+
32+
public async testHandleFailedResponse(response: Response, apiUrl: string) {
33+
return this.handleFailedResponse(response, apiUrl);
34+
}
35+
36+
public async testBuildApiUrlAndFetch(params: any) {
37+
return (this as any).buildApiUrlAndFetch(params);
38+
}
39+
}
40+
41+
describe('ApiClient', () => {
42+
let client: TestApiClient;
43+
44+
beforeEach(() => {
45+
client = new TestApiClient({});
46+
});
47+
48+
describe('parseResponseBodyJson', () => {
49+
it('should return JSON when response has valid JSON content', async () => {
50+
const response = new Response(JSON.stringify({ key: 'value' }), {
51+
status: StatusCodes.OK
52+
});
53+
54+
const result = await client.testParseResponseBodyJson(
55+
response,
56+
'http://example.com'
57+
);
58+
expect(result).toEqual({ key: 'value' });
59+
});
60+
61+
it('should return undefined when response is NO_CONTENT', async () => {
62+
const response = new Response(undefined, {
63+
status: StatusCodes.NO_CONTENT
64+
});
65+
66+
const result = await client.testParseResponseBodyJson(
67+
response,
68+
'http://example.com'
69+
);
70+
expect(result).toBeUndefined();
71+
});
72+
73+
it('should return undefined when response body is empty', async () => {
74+
const response = new Response('', {
75+
status: StatusCodes.OK
76+
});
77+
78+
const result = await client.testParseResponseBodyJson(
79+
response,
80+
'http://example.com'
81+
);
82+
expect(result).toBeUndefined();
83+
});
84+
85+
it('should throw error when response content is not valid JSON', async () => {
86+
const response = new Response('Not JSON content', {
87+
status: StatusCodes.OK
88+
});
89+
90+
await expect(
91+
client.testParseResponseBodyJson(response, 'http://example.com')
92+
).rejects.toMatchObject({
93+
status: StatusCodes.INTERNAL_SERVER_ERROR,
94+
message: 'Failed to convert response body to JSON'
95+
});
96+
});
97+
});
98+
99+
describe('getStatusCodeFromErrorMessage', () => {
100+
it('should return INTERNAL_SERVER_ERROR when error is undefined', async () => {
101+
const result = await client.testGetStatusCodeFromErrorMessage(undefined);
102+
expect(result).toBe(StatusCodes.INTERNAL_SERVER_ERROR);
103+
});
104+
105+
it('should return INTERNAL_SERVER_ERROR when error has no code or message', async () => {
106+
const result = await client.testGetStatusCodeFromErrorMessage({});
107+
expect(result).toBe(StatusCodes.INTERNAL_SERVER_ERROR);
108+
});
109+
110+
it('should map ENOTFOUND error to BAD_GATEWAY', async () => {
111+
const error = {
112+
code: 'ENOTFOUND',
113+
message: 'request to https://example.com failed, getaddrinfo ENOTFOUND'
114+
};
115+
116+
const result = await client.testGetStatusCodeFromErrorMessage(error);
117+
expect(result).toBe(StatusCodes.BAD_GATEWAY);
118+
});
119+
120+
it('should return INTERNAL_SERVER_ERROR when no pattern matches', async () => {
121+
const error = {
122+
code: 'UNKNOWN_ERROR',
123+
message: 'Some unknown error occurred'
124+
};
125+
126+
const result = await client.testGetStatusCodeFromErrorMessage(error);
127+
expect(result).toBe(StatusCodes.INTERNAL_SERVER_ERROR);
128+
});
129+
});
130+
131+
describe('handleNetworkError', () => {
132+
it('should preserve status code when error has valid status', async () => {
133+
const error = {
134+
status: StatusCodes.SERVICE_UNAVAILABLE,
135+
message: 'Service unavailable'
136+
};
137+
138+
const result = await client.testHandleNetworkError(error);
139+
expect(result.status).toBe(StatusCodes.SERVICE_UNAVAILABLE);
140+
expect(result.message).toBe('Service unavailable');
141+
});
142+
143+
it('should map ENOTFOUND to BAD_GATEWAY', async () => {
144+
const error = {
145+
code: 'ENOTFOUND',
146+
message:
147+
'request to https://api.example.com failed, getaddrinfo ENOTFOUND'
148+
};
149+
150+
const result = await client.testHandleNetworkError(error);
151+
expect(result.status).toBe(StatusCodes.BAD_GATEWAY);
152+
});
153+
});
154+
155+
describe('handleFailedResponse', () => {
156+
it('should throw HttpError with response details', async () => {
157+
const response = new Response('Bad request error', {
158+
status: StatusCodes.BAD_REQUEST
159+
});
160+
161+
await expect(
162+
client.testHandleFailedResponse(response, 'http://example.com/api')
163+
).rejects.toMatchObject({
164+
status: StatusCodes.BAD_REQUEST
165+
});
166+
});
167+
});
168+
169+
describe('setBaseUrl', () => {
170+
it('should update base URL', () => {
171+
client.setBaseUrl('https://api.newdomain.com');
172+
// No error should be thrown
173+
expect(client).toBeDefined();
174+
});
175+
});
176+
177+
describe('buildApiUrlAndFetch', () => {
178+
beforeEach(() => {
179+
jest.clearAllMocks();
180+
});
181+
182+
it('should call handleNetworkError when fetch throws an error', async () => {
183+
const networkError = new Error('Network failure');
184+
mockFetch.mockRejectedValue(networkError);
185+
186+
const handleNetworkErrorSpy = jest.spyOn(
187+
client as any,
188+
'handleNetworkError'
189+
);
190+
191+
await expect(
192+
client.testBuildApiUrlAndFetch({
193+
url: 'http://example.com/api',
194+
method: HttpMethod.GET
195+
})
196+
).rejects.toMatchObject({
197+
status: StatusCodes.INTERNAL_SERVER_ERROR
198+
});
199+
200+
expect(handleNetworkErrorSpy).toHaveBeenCalledWith(networkError);
201+
});
202+
203+
it('should call handleFailedResponse when response is not ok', async () => {
204+
const failedResponse = new Response('Bad request', {
205+
status: StatusCodes.BAD_REQUEST
206+
});
207+
mockFetch.mockResolvedValue(failedResponse);
208+
209+
const handleFailedResponseSpy = jest.spyOn(
210+
client as any,
211+
'handleFailedResponse'
212+
);
213+
214+
await expect(
215+
client.testBuildApiUrlAndFetch({
216+
url: 'http://example.com/api',
217+
method: HttpMethod.GET
218+
})
219+
).rejects.toMatchObject({
220+
status: StatusCodes.BAD_REQUEST
221+
});
222+
223+
expect(handleFailedResponseSpy).toHaveBeenCalledWith(
224+
failedResponse,
225+
'http://example.com/api'
226+
);
227+
});
228+
});
229+
});

0 commit comments

Comments
 (0)