diff --git a/jest.config.js b/jest.config.js
index 7d5d4e8f..22055d14 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,5 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
- preset: "ts-jest",
+ preset: "ts-jest/presets/js-with-ts-esm",
testEnvironment: "jest-environment-jsdom",
};
diff --git a/package-lock.json b/package-lock.json
index 87ccf710..3cc44e09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
"husky": "^8.0.3",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
+ "jest-fetch-mock": "^3.0.3",
"prettier": "^2.8.3",
"prettier-plugin-organize-imports": "^3.2.2",
"storybook": "^7.6.17",
@@ -60,9 +61,9 @@
"@mui/material": "^6.0.2",
"@tanstack/react-table": "^8.19.2",
"@tanstack/react-virtual": "^3.0.0-beta.59",
- "axios": "^1.6.7",
"copy-to-clipboard": "3.3.1",
"isomorphic-dompurify": "0.24.0",
+ "ky": "^1.7.2",
"next": "^14.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -10377,17 +10378,6 @@
"node": ">=4"
}
},
- "node_modules/axios": {
- "version": "1.7.7",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
- "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
- "peer": true,
- "dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -11883,9 +11873,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true,
"engines": {
"node": ">= 0.6"
@@ -12015,6 +12005,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "node_modules/cross-fetch": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+ "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+ "dev": true,
+ "dependencies": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -14338,9 +14337,9 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
@@ -14348,7 +14347,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -14776,26 +14775,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/follow-redirects": {
- "version": "1.15.6",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
- "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "peer": true,
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -17533,6 +17512,16 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/jest-fetch-mock": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+ "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+ "dev": true,
+ "dependencies": {
+ "cross-fetch": "^3.0.4",
+ "promise-polyfill": "^8.1.3"
+ }
+ },
"node_modules/jest-get-type": {
"version": "29.4.2",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz",
@@ -19067,6 +19056,18 @@
"node": ">= 8"
}
},
+ "node_modules/ky": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/ky/-/ky-1.7.2.tgz",
+ "integrity": "sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/ky?sponsor=1"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -22164,6 +22165,12 @@
"node": ">=0.4.0"
}
},
+ "node_modules/promise-polyfill": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
+ "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
+ "dev": true
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -22208,7 +22215,8 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
},
"node_modules/psl": {
"version": "1.9.0",
@@ -33639,17 +33647,6 @@
"integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==",
"dev": true
},
- "axios": {
- "version": "1.7.7",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
- "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
- "peer": true,
- "requires": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
"axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -34789,9 +34786,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true
},
"cookie-signature": {
@@ -34900,6 +34897,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "cross-fetch": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+ "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+ "dev": true,
+ "requires": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -36622,9 +36628,9 @@
}
},
"express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dev": true,
"requires": {
"accepts": "~1.3.8",
@@ -36632,7 +36638,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -37006,12 +37012,6 @@
"integrity": "sha512-WHRizzSrWFTcKo7cVcbP3wzZVhzsoYxoWqbnH4z+JXGqrjVmnsld6kBZWVlB200PwD5ur8r+HV3KUDxv3cHhOQ==",
"dev": true
},
- "follow-redirects": {
- "version": "1.15.6",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
- "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
- "peer": true
- },
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -38923,6 +38923,16 @@
}
}
},
+ "jest-fetch-mock": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+ "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+ "dev": true,
+ "requires": {
+ "cross-fetch": "^3.0.4",
+ "promise-polyfill": "^8.1.3"
+ }
+ },
"jest-get-type": {
"version": "29.4.2",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.2.tgz",
@@ -40060,6 +40070,12 @@
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
"dev": true
},
+ "ky": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/ky/-/ky-1.7.2.tgz",
+ "integrity": "sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==",
+ "peer": true
+ },
"language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -42261,6 +42277,12 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
+ "promise-polyfill": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
+ "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
+ "dev": true
+ },
"prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -42301,7 +42323,8 @@
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
},
"psl": {
"version": "1.9.0",
diff --git a/package.json b/package.json
index d292d078..49b37fc5 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "12.0.0",
"description": "",
"scripts": {
- "test": "jest",
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"lint": "eslint .",
"check-format": "prettier --check .",
"storybook": "storybook dev -p 6006",
@@ -57,6 +57,7 @@
"husky": "^8.0.3",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
+ "jest-fetch-mock": "^3.0.3",
"prettier": "^2.8.3",
"prettier-plugin-organize-imports": "^3.2.2",
"storybook": "^7.6.17",
@@ -70,9 +71,9 @@
"@mui/material": "^6.0.2",
"@tanstack/react-table": "^8.19.2",
"@tanstack/react-virtual": "^3.0.0-beta.59",
- "axios": "^1.6.7",
"copy-to-clipboard": "3.3.1",
"isomorphic-dompurify": "0.24.0",
+ "ky": "^1.7.2",
"next": "^14.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/src/components/TempError/tempError.tsx b/src/components/TempError/tempError.tsx
index 477807ec..8bfd1149 100644
--- a/src/components/TempError/tempError.tsx
+++ b/src/components/TempError/tempError.tsx
@@ -1,19 +1,20 @@
-import { AxiosError, isAxiosError } from "axios";
+import { HTTPError } from "ky";
import React from "react";
import { ErrorBox } from "./components/errorBox";
interface TempErrorProps {
- error: Error | AxiosError;
+ error: Error | HTTPError;
}
export const TempError = ({ error }: TempErrorProps): JSX.Element => {
- const { code, request } = isAxiosError(error)
- ? {
- ...error,
- code: error.response?.status,
- request: error.request.responseURL,
- }
- : { ...error, code: null, request: null };
+ const { code, request } =
+ error instanceof HTTPError
+ ? {
+ ...error,
+ code: error.response.status,
+ request: error.response.url,
+ }
+ : { ...error, code: null, request: null };
return (
diff --git a/src/config/entities.ts b/src/config/entities.ts
index 6b435593..0f55d952 100644
--- a/src/config/entities.ts
+++ b/src/config/entities.ts
@@ -139,7 +139,6 @@ export interface DataSourceConfig {
defaultParams?: {
catalog: string;
};
- entityURL?: string;
url: string;
}
diff --git a/src/entity/api/service.ts b/src/entity/api/service.ts
index c70ab94f..5a570313 100644
--- a/src/entity/api/service.ts
+++ b/src/entity/api/service.ts
@@ -16,12 +16,11 @@ import { FilterState } from "../../hooks/useCategoryFilter";
import {
getDefaultDetailParams,
getDefaultListParams,
- getEntityURL,
} from "../../shared/utils";
import { convertUrlParams } from "../../utils/url";
-import { api } from "../common/client";
+import { fetchApi } from "../common/client";
import { fetchEntitiesFromURL } from "../common/service";
-import { getAxiosRequestOptions } from "../common/utils";
+import { getKyRequestOptions } from "../common/utils";
/**
* Make a GET or POST request for a list of entities
@@ -70,9 +69,9 @@ export const fetchAllEntities = async (
let hits = result.hits;
let nextPage = result.pagination.next;
while (nextPage) {
- const { data: nextPageJson } = await api().get
(
- nextPage
- );
+ const nextPageJson = await (
+ await fetchApi(nextPage)
+ ).json();
nextPage = nextPageJson.pagination.next;
hits = [...hits, ...nextPageJson.hits];
}
@@ -84,8 +83,8 @@ export const fetchAllEntities = async (
* @returns name of the default catalog and all available catalogs.
*/
export const fetchCatalog = async (): Promise => {
- const res = await api().get(APIEndpoints.CATALOGS);
- return res.data;
+ const res = await fetchApi(APIEndpoints.CATALOGS);
+ return await res.json();
};
/**
@@ -108,18 +107,16 @@ export const fetchEntityDetail = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- this response type can't be determined beforehand
): Promise => {
const catalogParam = catalog ? { [AZUL_PARAM.CATALOG]: catalog } : undefined;
- const options = getAxiosRequestOptions(accessToken);
- const baseURL = getEntityURL();
- return await api(baseURL)
- .get(
- `${apiPath}/${id}?${convertUrlParams({
- ...defaultParams,
- ...catalogParam,
- })}`,
- options
- )
+ const options = getKyRequestOptions(accessToken);
+ return await fetchApi(
+ `${apiPath}/${id}?${convertUrlParams({
+ ...defaultParams,
+ ...catalogParam,
+ })}`,
+ options
+ )
.then((res) => {
- return res.data;
+ return res.json();
})
.catch((error) => {
if (swallow404) {
@@ -162,12 +159,12 @@ export const fetchSummary = async (
};
}
- const options = getAxiosRequestOptions(accessToken);
- const res = await api().get(
+ const options = getKyRequestOptions(accessToken);
+ const res = await fetchApi(
`${apiPath}?${convertUrlParams({ ...summaryParams })}`,
options
);
- return res.data;
+ return await res.json();
};
/**
@@ -180,11 +177,11 @@ export const fetchSummaryFromURL = async (
path: string,
accessToken: string | undefined
): Promise => {
- const res = await api().get(
+ const res = await fetchApi(
path,
- getAxiosRequestOptions(accessToken)
+ getKyRequestOptions(accessToken)
);
- return res.data;
+ return await res.json();
};
/**
@@ -193,6 +190,6 @@ export const fetchSummaryFromURL = async (
* @returns system status.
*/
export const fetchSystemStatusFromURL = async (URL: string): Promise => {
- const res = await api().get(URL);
- return res.data;
+ const res = await fetchApi(URL);
+ return await res.json();
};
diff --git a/src/entity/common/client.ts b/src/entity/common/client.ts
index 1e065b7a..c35000b6 100644
--- a/src/entity/common/client.ts
+++ b/src/entity/common/client.ts
@@ -1,48 +1,30 @@
-import axios, {
- AxiosError,
- AxiosInstance,
- AxiosResponse,
- HttpStatusCode,
-} from "axios";
+import ky, { KyInstance, Options, ResponsePromise } from "ky";
import { getURL } from "../../shared/utils";
-let axiosInstance: AxiosInstance | null = null;
+let kyInstance: KyInstance | null = null;
/**
- * Adding response interceptors to axios instances.
- * @param api - AxiosInstance.
+ * Makes an HTTP request with the API URL as a base.
+ * @param url - URL to fetch.
+ * @param options - Ky options.
+ * @returns Ky response.
*/
-export const configureInterceptors = (api: AxiosInstance): void => {
- api.interceptors.response.use(
- (response: AxiosResponse) => response,
- (error: AxiosError) => {
- const { config, response } = error;
-
- if (response?.status === HttpStatusCode.ServiceUnavailable && config) {
- const retryAfterValue = response.headers["Retry-After"];
- const waitingTime = retryAfterValue ? +retryAfterValue : 0;
- return new Promise((resolve) => {
- setTimeout(() => resolve(api(config)), waitingTime);
- });
- } else {
- return Promise.reject(error);
- }
- }
- );
-};
-
-/**
- * Returns a singleton Axios instance configured for making HTTP requests to a specified base URL.
- * @param baseURL - The base URL to use for the AxiosInstance.
- * @returns axios instance.
- */
-export const api = (baseURL = getURL()): AxiosInstance => {
- if (!axiosInstance) {
- axiosInstance = axios.create({
- baseURL,
+export function fetchApi(
+ url: string,
+ options: Options = {}
+): ResponsePromise {
+ if (!kyInstance) {
+ kyInstance = ky.create({
+ prefixUrl: getURL(),
+ retry: {
+ delay: (attemptCount) => 1000 * 3 ** (attemptCount - 1),
+ limit: 3,
+ },
timeout: 20 * 1000,
});
- configureInterceptors(axiosInstance);
}
- return axiosInstance;
-};
+ // If a full URL is provided, there shouldn't be a prefix URL. Otherwise, Ky requires that the URL not start with a slash.
+ if (/^https?:\/\//.test(url)) options = { prefixUrl: "", ...options };
+ else url = url.replace(/^\//, "");
+ return kyInstance.get(url, options);
+}
diff --git a/src/entity/common/service.ts b/src/entity/common/service.ts
index 37460f0d..bb666427 100644
--- a/src/entity/common/service.ts
+++ b/src/entity/common/service.ts
@@ -2,9 +2,8 @@ import {
AzulEntitiesResponse,
AzulEntityStaticResponse,
} from "../../apis/azul/common/entities";
-import { getEntityURL } from "../../shared/utils";
-import { api } from "./client";
-import { getAxiosRequestOptions } from "./utils";
+import { fetchApi } from "./client";
+import { getKyRequestOptions } from "./utils";
/**
* Fetch entities from the given URL.
@@ -16,11 +15,11 @@ export const fetchEntitiesFromURL = async (
URL: string,
accessToken?: string
): Promise => {
- const res = await api().get(
+ const res = await fetchApi(
URL,
- getAxiosRequestOptions(accessToken)
+ getKyRequestOptions(accessToken)
);
- return res.data;
+ return await res.json();
};
/**
@@ -33,10 +32,9 @@ export const fetchEntityFromURL = async (
URL: string,
accessToken?: string
): Promise => {
- const baseURL = getEntityURL();
- const res = await api(baseURL).get(
+ const res = await fetchApi(
URL,
- getAxiosRequestOptions(accessToken)
+ getKyRequestOptions(accessToken)
);
- return res.data;
+ return await res.json();
};
diff --git a/src/entity/common/utils.ts b/src/entity/common/utils.ts
index 5179c788..9927d73b 100644
--- a/src/entity/common/utils.ts
+++ b/src/entity/common/utils.ts
@@ -1,13 +1,11 @@
-import { AxiosRequestConfig } from "axios";
+import { Options } from "ky";
/**
- * Returns Axios request configuration.
+ * Returns Ky request configuration.
* @param accessToken - Access token.
- * @returns Axios request configuration.
+ * @returns Ky request configuration.
*/
-export function getAxiosRequestOptions(
- accessToken: string | undefined
-): AxiosRequestConfig {
+export function getKyRequestOptions(accessToken: string | undefined): Options {
return {
headers: accessToken ? { Authorization: "Bearer " + accessToken } : {},
};
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index e7e6b5a3..12fabc14 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -25,15 +25,6 @@ export function getDefaultListParams():
return { ...defaultListParams, ...defaultParams };
}
-/**
- * Returns entity source URL.
- * @returns entity source URL.
- */
-export function getEntityURL(): string {
- const dataSource = getConfig().dataSource;
- return dataSource.entityURL || dataSource.url;
-}
-
/**
* Returns application URL.
* @returns application url.
diff --git a/tests/authentication.test.ts b/tests/authentication.test.ts
index df08ccc7..358f9d63 100644
--- a/tests/authentication.test.ts
+++ b/tests/authentication.test.ts
@@ -1,3 +1,4 @@
+import { jest } from "@jest/globals";
import { LOGIN_STATUS_NOT_STARTED } from "../src/hooks/useAuthentication/common/constants";
import {
LoginStatus,
@@ -6,7 +7,12 @@ import {
import { GoogleResponse } from "../src/hooks/useAuthentication/useFetchGoogleProfile";
import { TerraResponse } from "../src/hooks/useAuthentication/useFetchTerraProfile";
import { TerraTermsOfServiceResponse } from "../src/hooks/useAuthentication/useFetchTerraTermsOfService";
-import { shouldReleaseToken } from "../src/providers/authentication";
+
+jest.unstable_mockModule("react-idle-timer", () => ({
+ useIdleTimer: jest.fn(),
+}));
+
+const { shouldReleaseToken } = await import("../src/providers/authentication");
describe("authentication", () => {
// Boolean constants.
diff --git a/tests/azulFileDownload.test.tsx b/tests/azulFileDownload.test.tsx
index 649a72d4..312cc03e 100644
--- a/tests/azulFileDownload.test.tsx
+++ b/tests/azulFileDownload.test.tsx
@@ -1,15 +1,21 @@
+import { jest } from "@jest/globals";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react";
-import { AzulFileDownload } from "../src/components/Index/components/AzulFileDownload/azulFileDownload";
import {
AZUL_FILE_DOWNLOAD_TEST_ID,
AZUL_FILE_REQUEST_DOWNLOAD_PENDING_TEST_ID,
AZUL_FILE_REQUEST_DOWNLOAD_TEST_ID,
} from "../src/components/Index/components/AzulFileDownload/common/constants";
-import { useFileLocation } from "../src/hooks/useFileLocation";
import { getAnchorEl, getButtonById } from "../src/utils/tests";
-jest.mock("../src/hooks/useFileLocation");
+jest.unstable_mockModule("../src/hooks/useFileLocation", () => ({
+ useFileLocation: jest.fn(),
+}));
+
+const { AzulFileDownload } = await import(
+ "../src/components/Index/components/AzulFileDownload/azulFileDownload"
+);
+const { useFileLocation } = await import("../src/hooks/useFileLocation");
describe("AzulFileDownload", () => {
const FILE_URL = "https://example.com/storage/file";
diff --git a/tests/fetchApi.test.ts b/tests/fetchApi.test.ts
new file mode 100644
index 00000000..9e719b59
--- /dev/null
+++ b/tests/fetchApi.test.ts
@@ -0,0 +1,93 @@
+import { jest } from "@jest/globals";
+import fetchMock from "jest-fetch-mock";
+
+jest.unstable_mockModule("../src/shared/utils", () => ({
+ getURL: (): string => "http://example.com",
+}));
+
+const { fetchApi } = await import("../src/entity/common/client");
+
+const actualSetTimeout = setTimeout;
+
+const mockSetTimeout = jest
+ .spyOn(globalThis, "setTimeout")
+ .mockImplementation(((callback: () => void, timeout: number) => {
+ if (timeout === 20000) actualSetTimeout(callback, timeout);
+ else callback();
+ }) as typeof setTimeout);
+
+beforeAll(async () => {
+ fetchMock.doMock();
+ globalThis.fetch = ((...args) => {
+ return fetchMock(...(args as Parameters));
+ }) as typeof fetch;
+ AbortSignal.prototype.throwIfAborted = function (): void {
+ if (this.aborted) throw this.reason;
+ };
+});
+
+describe("api", () => {
+ it("returns immediately-successful response", async () => {
+ fetchMock.mockResponseOnce("immediately-successful");
+ const res = await fetchApi("");
+ expect(res.status).toBe(200);
+ expect(await res.text()).toBe("immediately-successful");
+ });
+
+ it("returns response after backing off from three 503 responses", async () => {
+ mockSetTimeout.mockClear();
+
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("after-three-503");
+ const res = await fetchApi("");
+ expect(res.status).toBe(200);
+ expect(await res.text()).toBe("after-three-503");
+
+ expect(mockSetTimeout).toBeCalledTimes(7);
+ expect(mockSetTimeout.mock.calls[1][1]).toBe(1000);
+ expect(mockSetTimeout.mock.calls[3][1]).toBe(3000);
+ expect(mockSetTimeout.mock.calls[5][1]).toBe(9000);
+ });
+
+ it("throws error after four 503 responses", async () => {
+ mockSetTimeout.mockClear();
+
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("", { status: 503 });
+ const error = await fetchApi("").catch((e) => e);
+ expect(error).toMatchObject({ response: { status: 503 } });
+
+ expect(mockSetTimeout).toBeCalledTimes(7);
+ });
+
+ it("throws error after one 503 response and one 400 response", async () => {
+ mockSetTimeout.mockClear();
+
+ fetchMock.mockResponseOnce("", { status: 503 });
+ fetchMock.mockResponseOnce("", { status: 400 });
+ const error = await fetchApi("").catch((e) => e);
+ expect(error).toMatchObject({ response: { status: 400 } });
+
+ expect(mockSetTimeout).toBeCalledTimes(3);
+ });
+
+ it("returns response after delaying based on Retry-After", async () => {
+ mockSetTimeout.mockClear();
+
+ fetchMock.mockResponseOnce("", {
+ headers: { "Retry-After": "7" },
+ status: 503,
+ });
+ fetchMock.mockResponseOnce("after-retry-after");
+ const res = await fetchApi("");
+ expect(res.status).toBe(200);
+ expect(await res.text()).toBe("after-retry-after");
+
+ expect(mockSetTimeout).toBeCalledTimes(3);
+ expect(mockSetTimeout.mock.calls[1][1]).toBe(7000);
+ });
+});
diff --git a/tests/tsconfig.json b/tests/tsconfig.json
index 57fa38e0..f67c35d5 100644
--- a/tests/tsconfig.json
+++ b/tests/tsconfig.json
@@ -1,4 +1,5 @@
{
+ "allowJs": true,
"compilerOptions": {
"rootDir": "../"
},
diff --git a/tests/useFileLocation.test.ts b/tests/useFileLocation.test.ts
index bb21992f..329c5e75 100644
--- a/tests/useFileLocation.test.ts
+++ b/tests/useFileLocation.test.ts
@@ -1,10 +1,14 @@
-import { buildFetchFileUrl } from "../src/hooks/useFileLocation";
+import { jest } from "@jest/globals";
const URL_INVALID = "invalidUrl";
const URL_WITH_FETCH_PREPEND = "https://example.com/fetch/repository/file";
const URL_WITHOUT_FETCH_PREPEND = "https://example.com/repository/file";
-jest.mock("../src/hooks/useRequestFileLocation");
+jest.unstable_mockModule("../src/hooks/useRequestFileLocation", () => ({
+ useRequestFileLocation: jest.fn(),
+}));
+
+const { buildFetchFileUrl } = await import("../src/hooks/useFileLocation");
describe("useFileLocation", () => {
describe("build file URL", () => {