diff --git a/site/package.json b/site/package.json index 175e0e33ad..664c5ded39 100644 --- a/site/package.json +++ b/site/package.json @@ -17,12 +17,14 @@ "storybook": "start-storybook -p 6006 -s ./static", "storybook:build": "build-storybook", "test": "jest --selectProjects test", - "test:coverage": "jest --selectProjects test --collectCoverage" + "test:coverage": "jest --selectProjects test --collectCoverage", + "test:watch": "jest --selectProjects test --watch" }, "dependencies": { "@material-ui/core": "4.9.4", "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.42", + "axios": "0.26.1", "formik": "2.2.9", "history": "5.3.0", "react": "17.0.2", diff --git a/site/src/api.test.ts b/site/src/api.test.ts new file mode 100644 index 0000000000..68da3fa56d --- /dev/null +++ b/site/src/api.test.ts @@ -0,0 +1,124 @@ +import axios from "axios" +import { APIKeyResponse, getApiKey, login, LoginResponse, logout } from "./api" + +// Mock the axios module so that no real network requests are made, but rather +// we swap in a resolved or rejected value +// +// See: https://jestjs.io/docs/mock-functions#mocking-modules +jest.mock("axios") + +describe("api.ts", () => { + describe("login", () => { + it("should return LoginResponse", async () => { + // given + const loginResponse: LoginResponse = { + session_token: "abc_123_test", + } + const axiosMockPost = jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ data: loginResponse }) + }) + axios.post = axiosMockPost + + // when + const result = await login("test", "123") + + // then + expect(axiosMockPost).toHaveBeenCalled() + expect(result).toStrictEqual(loginResponse) + }) + + it("should throw an error on 401", async () => { + // given + // ..ensure that we await our expect assertion in async/await test + expect.assertions(1) + const expectedError = { + message: "Validation failed", + errors: [{ field: "email", code: "email" }], + } + const axiosMockPost = jest.fn().mockImplementationOnce(() => { + return Promise.reject(expectedError) + }) + axios.post = axiosMockPost + + try { + await login("test", "123") + } catch (error) { + expect(error).toStrictEqual(expectedError) + } + }) + }) + + describe("logout", () => { + it("should return without erroring", async () => { + // given + const axiosMockPost = jest.fn().mockImplementationOnce(() => { + return Promise.resolve() + }) + axios.post = axiosMockPost + + // when + await logout() + + // then + expect(axiosMockPost).toHaveBeenCalled() + }) + + it("should throw an error on 500", async () => { + // given + // ..ensure that we await our expect assertion in async/await test + expect.assertions(1) + const expectedError = { + message: "Failed to logout.", + } + const axiosMockPost = jest.fn().mockImplementationOnce(() => { + return Promise.reject(expectedError) + }) + axios.post = axiosMockPost + + try { + await logout() + } catch (error) { + expect(error).toStrictEqual(expectedError) + } + }) + }) + + describe("getApiKey", () => { + it("should return APIKeyResponse", async () => { + // given + const apiKeyResponse: APIKeyResponse = { + key: "abc_123_test", + } + const axiosMockPost = jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ data: apiKeyResponse }) + }) + axios.post = axiosMockPost + + // when + const result = await getApiKey() + + // then + expect(axiosMockPost).toHaveBeenCalled() + expect(result).toStrictEqual(apiKeyResponse) + }) + + it("should throw an error on 401", async () => { + // given + // ..ensure that we await our expect assertion in async/await test + expect.assertions(1) + const expectedError = { + message: "No Cookie!", + } + const axiosMockPost = jest.fn().mockImplementationOnce(() => { + return Promise.reject(expectedError) + }) + axios.post = axiosMockPost + + try { + await getApiKey() + } catch (error) { + expect(error).toStrictEqual(expectedError) + } + }) + }) +}) diff --git a/site/src/api.ts b/site/src/api.ts index 179bc1ce9a..3cdfd59613 100644 --- a/site/src/api.ts +++ b/site/src/api.ts @@ -1,7 +1,8 @@ +import axios, { AxiosRequestHeaders } from "axios" import { mutate } from "swr" -interface LoginResponse { - session_token: string +const CONTENT_TYPE_JSON: AxiosRequestHeaders = { + "Content-Type": "application/json", } /** @@ -107,48 +108,32 @@ export namespace Workspace { } } +export interface LoginResponse { + session_token: string +} + export const login = async (email: string, password: string): Promise => { - const response = await fetch("/api/v2/users/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - password, - }), + const payload = JSON.stringify({ + email, + password, }) - const body = await response.json() - if (!response.ok) { - throw new Error(body.message) - } + const response = await axios.post("/api/v2/users/login", payload, { + headers: { ...CONTENT_TYPE_JSON }, + }) - return body + return response.data } export const logout = async (): Promise => { - const response = await fetch("/api/v2/users/logout", { - method: "POST", - }) - - if (!response.ok) { - const body = await response.json() - throw new Error(body.message) - } - - return + await axios.post("/api/v2/users/logout") } -export const getApiKey = async (): Promise<{ key: string }> => { - const response = await fetch("/api/v2/users/me/keys", { - method: "POST", - }) - - if (!response.ok) { - const body = await response.json() - throw new Error(body.message) - } - - return await response.json() +export interface APIKeyResponse { + key: string +} + +export const getApiKey = async (): Promise => { + const response = await axios.post("/api/v2/users/me/keys") + return response.data } diff --git a/site/yarn.lock b/site/yarn.lock index 8b1590bdb7..39a8e904ab 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -4085,6 +4085,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.0.tgz#f93be7f81017eb8bedeb1859cc8092cc918d2dc8" integrity sha512-btWy2rze3NnxSSxb7LtNhPYYFrRoFBfjiGzmSc/5Hu47wApO2KNXjP/w7Nv2Uz/Fyr/pfEiwOkcXhDxu0jz5FA== +axios@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -6773,6 +6780,11 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +follow-redirects@^1.14.8: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"