Skip to content

Commit

Permalink
feat: add API thunks for JIMM endpoints (#1767)
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi authored Jun 5, 2024
1 parent 4716c06 commit bea5ae0
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 1 deletion.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@
"vite-plugin-html": "3.2.2",
"vite-plugin-node-polyfills": "0.22.0",
"vite-tsconfig-paths": "4.3.2",
"vitest": "1.6.0"
"vitest": "1.6.0",
"vitest-fetch-mock": "0.2.2"
},
"npmpackagejsonlint": {
"rules": {
Expand Down
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "vitest-fetch-mock";
5 changes: 5 additions & 0 deletions src/juju/jimm/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const endpoints = {
login: "/auth/login",
logout: "/auth/logout",
whoami: "/auth/whoami",
};
67 changes: 67 additions & 0 deletions src/juju/jimm/thunks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { unwrapResult } from "@reduxjs/toolkit";
import { vi } from "vitest";

import { endpoints } from "./api";
import { logout, whoami } from "./thunks";

describe("thunks", () => {
beforeEach(() => {
fetchMock.resetMocks();
});

it("logout", async () => {
const action = logout();
await action(vi.fn(), vi.fn(), null);
expect(global.fetch).toHaveBeenCalledWith(endpoints.logout);
});

it("logout handles unsuccessful requests", async () => {
fetchMock.mockResponseOnce(JSON.stringify({}), { status: 500 });
const action = logout();
const response = await action(vi.fn(), vi.fn(), null);
expect("error" in response ? response.error.message : null).toBe(
"Unable to log out: non-success response",
);
});

it("logout handles errors", async () => {
fetchMock.mockRejectedValue("404");
const action = logout();
const response = await action(vi.fn(), vi.fn(), null);
expect("error" in response ? response.error.message : null).toBe(
"Unable to log out: 404",
);
});

it("whoami returns a user", async () => {
const action = whoami();
await action(vi.fn(), vi.fn(), null);
expect(global.fetch).toHaveBeenCalledWith(endpoints.whoami);
});

it("whoami handles non-authenticated user", async () => {
fetchMock.mockResponseOnce(JSON.stringify({}), { status: 403 });
const action = whoami();
const response = await action(vi.fn(), vi.fn(), null);
expect(global.fetch).toHaveBeenCalledWith(endpoints.whoami);
expect(unwrapResult(response)).toBeNull();
});

it("whoami unsuccessful requests", async () => {
fetchMock.mockResponse(JSON.stringify({}), { status: 500 });
const action = whoami();
const response = await action(vi.fn(), vi.fn(), null);
expect("error" in response ? response.error.message : null).toBe(
"Unable to get user details: non-success response",
);
});

it("whoami handles errors", async () => {
fetchMock.mockRejectedValue("404");
const action = whoami();
const response = await action(vi.fn(), vi.fn(), null);
expect("error" in response ? response.error.message : null).toBe(
"Unable to get user details: 404",
);
});
});
51 changes: 51 additions & 0 deletions src/juju/jimm/thunks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createAsyncThunk } from "@reduxjs/toolkit";

import type { RootState } from "store/store";
import { toErrorString } from "utils";

import { endpoints } from "./api";

/**
Log out of the JIMM API.
*/
export const logout = createAsyncThunk<
void,
void,
{
state: RootState;
}
>("jimm/logout", async () => {
try {
const response = await fetch(endpoints.logout);
if (!response.ok) {
throw new Error("non-success response");
}
} catch (error) {
throw new Error(`Unable to log out: ${toErrorString(error)}`);
}
});

/**
Get the authenticated user from the JIMM API.
*/
export const whoami = createAsyncThunk<
void,
void,
{
state: RootState;
}
>("jimm/whoami", async () => {
try {
const response = await fetch(endpoints.whoami);
if (response.status === 403) {
// The user is not authenticated so return null instead of throwing an error.
return null;
}
if (!response.ok) {
throw new Error("non-success response");
}
return await response.json();
} catch (error) {
throw new Error(`Unable to get user details: ${toErrorString(error)}`);
}
});
7 changes: 7 additions & 0 deletions src/store/middleware/check-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { isAction, type Middleware } from "redux";

import * as jimmThunks from "juju/jimm/thunks";
import { actions as appActions, thunks as appThunks } from "store/app";
import { actions as generalActions } from "store/general";
import { isLoggedIn } from "store/general/selectors";
Expand Down Expand Up @@ -99,6 +100,12 @@ export const checkAuthMiddleware: Middleware<
addControllerCloudRegion.fulfilled.type,
addControllerCloudRegion.pending.type,
addControllerCloudRegion.rejected.type,
jimmThunks.logout.fulfilled.type,
jimmThunks.logout.pending.type,
jimmThunks.logout.rejected.type,
jimmThunks.whoami.fulfilled.type,
jimmThunks.whoami.pending.type,
jimmThunks.whoami.rejected.type,
];

const state = getState();
Expand Down
5 changes: 5 additions & 0 deletions src/testing/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import "@testing-library/jest-dom/vitest";
import type { Window as HappyDOMWindow } from "happy-dom";
import { vi } from "vitest";
import createFetchMock from "vitest-fetch-mock";

vi.mock("react-ga");

const fetchMocker = createFetchMock(vi);
// sets globalThis.fetch and globalThis.fetchMock to our mocked version
fetchMocker.enableMocks();

declare global {
interface Window extends HappyDOMWindow {}
// eslint-disable-next-line no-var
Expand Down
59 changes: 59 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3807,6 +3807,15 @@ __metadata:
languageName: node
linkType: hard

"cross-fetch@npm:^3.0.6":
version: 3.1.8
resolution: "cross-fetch@npm:3.1.8"
dependencies:
node-fetch: "npm:^2.6.12"
checksum: 10c0/4c5e022ffe6abdf380faa6e2373c0c4ed7ef75e105c95c972b6f627c3f083170b6886f19fb488a7fa93971f4f69dcc890f122b0d97f0bf5f41ca1d9a8f58c8af
languageName: node
linkType: hard

"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
Expand Down Expand Up @@ -7582,6 +7591,7 @@ __metadata:
vite-plugin-node-polyfills: "npm:0.22.0"
vite-tsconfig-paths: "npm:4.3.2"
vitest: "npm:1.6.0"
vitest-fetch-mock: "npm:0.2.2"
yup: "npm:1.4.0"
languageName: unknown
linkType: soft
Expand Down Expand Up @@ -8271,6 +8281,20 @@ __metadata:
languageName: node
linkType: hard

"node-fetch@npm:^2.6.12":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
languageName: node
linkType: hard

"node-gyp-build@npm:^4.3.0":
version: 4.6.0
resolution: "node-gyp-build@npm:4.6.0"
Expand Down Expand Up @@ -10935,6 +10959,13 @@ __metadata:
languageName: node
linkType: hard

"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
languageName: node
linkType: hard

"trim-newlines@npm:^3.0.0":
version: 3.0.1
resolution: "trim-newlines@npm:3.0.1"
Expand Down Expand Up @@ -11549,6 +11580,17 @@ __metadata:
languageName: node
linkType: hard

"vitest-fetch-mock@npm:0.2.2":
version: 0.2.2
resolution: "vitest-fetch-mock@npm:0.2.2"
dependencies:
cross-fetch: "npm:^3.0.6"
peerDependencies:
vitest: ">=0.16.0"
checksum: 10c0/5c011274089301e2c21e794e79de6af2ad2884c1bf5784f79f11a06f93b8dc0284f007645ab145708bab86c810356eaafaf9de31fe9746f9084dcf28c20f85e9
languageName: node
linkType: hard

"vitest@npm:1.6.0":
version: 1.6.0
resolution: "vitest@npm:1.6.0"
Expand Down Expand Up @@ -11615,6 +11657,13 @@ __metadata:
languageName: node
linkType: hard

"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
languageName: node
linkType: hard

"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
Expand Down Expand Up @@ -11662,6 +11711,16 @@ __metadata:
languageName: node
linkType: hard

"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
languageName: node
linkType: hard

"which-boxed-primitive@npm:^1.0.2":
version: 1.0.2
resolution: "which-boxed-primitive@npm:1.0.2"
Expand Down

0 comments on commit bea5ae0

Please sign in to comment.