Merge to main (#17)

* Fix sign up form bug

* Add run.sh to run setup and run web app

* Output node version

* Update readme with run.sh

* Fix file upload form to handle ota_update service

* Enable file upload form

Enable error boundary to catch React errors (#7)
Fix warning for link noreferrer
Include authorization header with file upload

* Remove default localhost settings (#8)

* Remove default localhost settings
Replace with deployment settings

* Fix for upload data format

* Fix test data for last commit

* Fix json link format and remove localhost default settings (#10)

* Remove default localhost settings
Replace with deployment settings

* Fix for upload data format

* Fix test data for last commit

* Fix link data format

* Fix link json again (#12)

Use id token instead of access token

* nginx things

* Web Worker Sign Out and Use Go API (#13)

* Calculate checksum and send with file upload

* Limit file upload and display rejected file error

* Add sign in timeout

* Check auth token structure before setting
Clean up

* Use web worker timer to sign out
Remove checksum
Point to Go ota update

* Remove checksum dependency

* Use compute auth service and fix static code analyzer warnings (#15)

* Clean up formatting

* Use new compute_auth service
Implment SSO
Implement token refresh
Clean up unit tests

* Fix unit tests

* Fix auth test
Fix warnings

* Update default settings for compute_auth

* Change main UI layout and add VINs to add and upload forms (#16)

* Add new upload update package form
Add new add vehicle form
Add new side menu layout
Add new toolbar layout
Update and add unit tests

* Enable add get and add vehicles

* Integration issues with ota_update service

* Update get vehicle JSON format

* Fix related unit test
Add release notes field

* Add StatusContext to display error and status messages

* Handle api error json (#18)

* Handle api error json

* Fix get vehicles error handling
Update .env.template

Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com>
This commit is contained in:
John Wu
2021-03-17 15:16:08 -07:00
committed by GitHub
parent 86d65b887c
commit 30155887cb
62 changed files with 4800 additions and 3716 deletions

View File

@@ -8,6 +8,8 @@ export const FileUploadProvider = ({ children }) => {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState(null);
const [cancelUpload, setCancelUpload] = useState(null);
const [linkURL, setLinkURL] = useState(null);
const [files, setFiles] = useState(null);
const done = () => {
setCancelUpload(null);
@@ -23,37 +25,80 @@ export const FileUploadProvider = ({ children }) => {
done();
};
const upload = async (files) => {
try {
if (!files || files.length === 0) throw new Error("No file provided");
const validateUpload = (formData, accessToken, uploadFiles) => {
if (!formData) {
throw new Error("Missing package update data");
}
const file = files[0].file;
if (!formData.packagename || formData.packagename.length === 0) {
throw new Error("Package name required");
}
if (!formData.version || formData.version.length === 0) {
throw new Error("Package update version required");
}
if (!formData.vehicles || formData.vehicles.length === 0) {
throw new Error("Vehicles required");
}
if (!uploadFiles || uploadFiles.length === 0) {
throw new Error("File required");
}
if (!accessToken || accessToken.length === 0) {
throw new Error("Access token required");
}
};
const upload = async (formData, accessToken, uploadFiles) => {
validateUpload(formData, accessToken, uploadFiles);
try {
const file = uploadFiles[0];
const filename = file.name;
setUploading(true);
setLinkURL(null);
setProgress(0);
setStatus(`Uploading ${filename}`);
setCancelUpload(getCancelToken());
const result = await uploadFile(file, setProgress, cancelUpload);
const url = ((result && result.url) ? result.url : "No URL available");
setStatus(`Uploaded ${filename}\n${url}`);
const { data } = await uploadFile(
file,
formData,
accessToken,
setProgress,
cancelUpload
);
if (data.message) {
throw new Error(`${data.error}. ${data.message}`);
}
const url = data && data.link ? data.link : "No URL available";
setLinkURL(url);
setStatus(`Uploaded ${filename}`);
setCancelUpload(null);
setProgress(100);
}
catch (e) {
} catch (e) {
setUploading(true);
setStatus(`Error occured: ${e.message}`);
setProgress(-1);
}
};
return (
<FileUploadContext.Provider value={{
uploading,
progress,
status,
upload,
cancel,
}}>
<FileUploadContext.Provider
value={{
uploading,
progress,
status,
linkURL,
files,
upload,
cancel,
setFiles,
}}
>
{children}
</FileUploadContext.Provider>
);

View File

@@ -1,26 +1,96 @@
jest.mock("../../services/uploadFile");
import {uploadFile, getCancelToken, setUploadFileResponse, setUploadFileDelay, getIssuedCancelToken } from "../../services/uploadFile"
import { FileUploadProvider, useFileUploadContext } from "../Contexts/FileUploadContext";
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { setUploadFileDelay } from "../../services/uploadFile";
import {
FileUploadProvider,
useFileUploadContext,
} from "../Contexts/FileUploadContext";
import { StatusProvider, useStatusContext } from "../Contexts/StatusContext";
const checkState = (uploading, progress, status, linkURL, message) => {
expect(screen.getByTestId("uploading").innerHTML).toEqual(uploading);
expect(screen.getByTestId("progress").innerHTML).toEqual(progress);
expect(screen.getByTestId("status").innerHTML).toEqual(status);
expect(screen.getByTestId("linkURL").innerHTML).toEqual(linkURL);
expect(screen.getByTestId("message").innerHTML).toEqual(message);
};
describe("FileUploadContext", () => {
beforeEach(() => {
const TestComp = () => {
const { progress, uploading, status, upload, cancel } = useFileUploadContext();
const {
progress,
uploading,
status,
linkURL,
upload,
cancel,
setFiles,
} = useFileUploadContext();
const { message, setMessage } = useStatusContext();
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
const TEST_ACCESSTOKEN = "ACCESSTOKEN";
const TEST_FORMDATA = {
packagename: "TEST",
version: "VERSION",
vehicles: ["VIN"],
};
const exec = async (form, token, file) => {
try {
await upload(form, token, file);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="uploading">{uploading.toString()}</div>
<div data-testid="progress">{progress.toString()}</div>
<div data-testid="status">{status}</div>
<button data-testid="uploadNoFile" onClick={() => upload()}/>
<button data-testid="upload" onClick={() => upload([{ file: { name: "test.jpg" }}])}/>
<button data-testid="cancel" onClick={() => cancel()}/>
<div data-testid="message">{message}</div>
<div data-testid="linkURL">{linkURL}</div>
<button
data-testid="uploadNoFile"
onClick={() => {
exec(TEST_FORMDATA, TEST_ACCESSTOKEN, null);
}}
/>
<button
data-testid="uploadNoToken"
onClick={() => {
exec(TEST_FORMDATA, null, TEST_FILE);
}}
/>
<button
data-testid="uploadNoFormData"
onClick={() => {
exec({}, TEST_ACCESSTOKEN, TEST_FILE);
}}
/>
<button
data-testid="upload"
onClick={() => exec(TEST_FORMDATA, TEST_ACCESSTOKEN, TEST_FILE)}
/>
<button data-testid="cancel" onClick={() => cancel()} />
</>
);
};
render(<FileUploadProvider><TestComp /></FileUploadProvider>);
render(
<StatusProvider>
<FileUploadProvider>
<TestComp />
</FileUploadProvider>
</StatusProvider>
);
});
afterEach(() => {
@@ -28,34 +98,53 @@ describe("FileUploadContext", () => {
});
it("Initial state", async () => {
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
expect(screen.getByTestId("progress").innerHTML).toEqual("0");
expect(screen.getByTestId("status").innerHTML).toEqual("");
})
checkState("false", "0", "", "", "");
});
it("Upload no file", async () => {
fireEvent.click(screen.getByTestId("uploadNoFile"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
expect(screen.getByTestId("progress").innerHTML).toEqual("0");
expect(screen.getByTestId("status").innerHTML).toEqual("Error occured: No file provided");
})
await waitFor(() =>
expect(screen.getByTestId("message").innerHTML).not.toBe("")
);
checkState("false", "0", "", "", "File required");
});
it("Upload no access token", async () => {
fireEvent.click(screen.getByTestId("uploadNoToken"));
await waitFor(() =>
expect(screen.getByTestId("message").innerHTML).not.toBe("")
);
checkState("false", "0", "", "", "Access token required");
});
it("Upload no form data", async () => {
fireEvent.click(screen.getByTestId("uploadNoFormData"));
await waitFor(() =>
expect(screen.getByTestId("message").innerHTML).not.toBe("")
);
checkState("false", "0", "", "", "Package name required");
});
it("Upload file", async () => {
fireEvent.click(screen.getByTestId("upload"));
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("100"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("true");
expect(screen.getByTestId("status").innerHTML).toEqual("Uploaded test.jpg\nCLOUDFRONT_URL");
})
await waitFor(() =>
expect(screen.getByTestId("progress").innerHTML).toEqual("100")
);
checkState("true", "100", "Uploaded test.jpg", "CLOUDFRONT_URL", "");
});
it("Cancel upload", async () => {
setUploadFileDelay(true);
fireEvent.click(screen.getByTestId("upload"));
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("50"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("true");
expect(screen.getByTestId("status").innerHTML).toEqual("Uploading test.jpg");
await waitFor(() =>
expect(screen.getByTestId("progress").innerHTML).toEqual("50")
);
checkState("true", "50", "Uploading test.jpg", "", "");
fireEvent.click(screen.getByTestId("cancel"));
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("0"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
expect(screen.getByTestId("status").innerHTML).toEqual("Upload cancelled");
})
})
await waitFor(() =>
expect(screen.getByTestId("progress").innerHTML).toEqual("0")
);
checkState("false", "0", "Upload cancelled", "", "");
});
});

View File

@@ -0,0 +1,20 @@
import React, { useContext, useState } from "react";
const StatusContext = React.createContext();
export const StatusProvider = ({ children }) => {
const [message, setMessage] = useState(null);
return (
<StatusContext.Provider
value={{
message,
setMessage,
}}
>
{children}
</StatusContext.Provider>
);
};
export const useStatusContext = () => useContext(StatusContext);

View File

@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import auth from '../../services/auth';
import React, { useContext, useEffect, useState } from "react";
import auth from "../../services/auth";
import getTimerWorker from "../../services/timer";
const UserContext = React.createContext();
@@ -7,87 +8,153 @@ export const UserProvider = ({ children }) => {
const [fetching, setFetching] = useState(false);
const [token, setToken] = useState(null);
const [error, setError] = useState(null);
let timer;
useEffect(() => {
if (!localStorage) return;
const token = JSON.parse(localStorage.getItem("token"));
if (!token) return;
const { accessToken: { jwtToken }} = token;
const verifyToken = async (accessToken) => {
const result = await auth.verify(accessToken);
if (result.authenticated) {
setToken(token);
} else {
await signOut();
}
};
verifyToken(jwtToken);
return () => {};
const t = JSON.parse(localStorage.getItem("token"));
if (!t || !t.idToken || !t.idToken.jwtToken) return;
if (!t.idToken.payload || !t.idToken.payload.exp) return;
setToken(t);
}, []);
const signIn = async (username, password) => {
useEffect(() => {
if (!token) return;
verifyToken();
return () => {
if (timer) timer.terminate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const refreshTokens = async () => {
if (!token || !token.refreshToken || !token.refreshToken.token) return null;
const result = await refresh(token.refreshToken.token);
return result;
};
const isError = (resp) => {
if (resp === null) return true;
if (resp && resp.error) return true;
return false;
};
const startSessionTimer = () => {
const duration = 1000 * token.idToken.payload.exp - new Date().getTime();
if (!timer) {
timer = getTimerWorker();
timer.onMessage(async (e) => {
if (e.data === "timeout") {
const t = await refreshTokens();
if (!isError(t)) return;
signOut();
}
});
}
timer.start(duration);
};
const verifyToken = async () => {
try {
if (!username) throw new Error('Email is required');
if (!password) throw new Error('Password is required');
const {
idToken: { jwtToken: idToken },
} = token;
const result = await auth.verify(idToken);
if (!result && !result.valid) {
const t = await refreshTokens();
if (!isError(t)) return;
signOut();
return;
}
startSessionTimer();
} catch (e) {
signOut();
setError(`Verify error. ${e.message}`);
}
};
const signIn = async (code) => {
let result = null;
try {
if (!code) return;
setFetching(true);
setError(null);
const result = await auth.signIn(username, password);
if (result.message) throw new Error(result.message);
result = await auth.signIn(code);
if (result.message) {
throw new Error(result.message);
}
signedIn(result);
}
catch (error) {
setError(error.message);
}
finally {
} catch (err) {
setError(`Sign in error. ${err.message}`);
} finally {
setFetching(false);
}
return result;
};
const signUp = async (username, password, confirmPassword) => {
try {
if (!username) throw new Error('Email is required');
if (!password) throw new Error('Password is required');
if (password !== confirmPassword) throw new Error('Passwords do not match');
const signOut = () => {
setToken(null);
if (localStorage) {
localStorage.removeItem("token");
}
return getLogoutURL();
};
const signedIn = (value) => {
setToken(value);
if (!localStorage || !value || !value.idToken) return;
localStorage.setItem("token", JSON.stringify(value));
};
const refresh = async (value) => {
let result = null;
try {
if (!value) {
throw new Error("Token required");
}
setFetching(true);
setError(null);
const result = await auth.signUp(username, password);
if (result.message) throw new Error(result.message);
}
catch (error) {
setError(error.message);
}
finally {
result = await auth.refresh(value);
if (result.message) {
throw new Error(result.message);
}
signedIn(result);
} catch (err) {
setError(`Refresh error. ${err.message}`);
} finally {
setFetching(false);
}
return result;
};
const signOut = async () => {
setToken(null);
if (!localStorage) return;
localStorage.removeItem("token");
};
const signedIn = (token) => {
setToken(token);
if (!localStorage || !token || !token.accessToken) return;
localStorage.setItem("token", JSON.stringify(token));
}
const getAuthorizeURL = () => auth.ssoAuthorize();
const getLogoutURL = () => auth.ssoLogout();
return (
<UserContext.Provider value={{
fetching,
token,
error,
setError,
signIn,
signUp,
signOut,
}}>
<UserContext.Provider
value={{
fetching,
token,
error,
setError,
signIn,
signOut,
refresh,
getAuthorizeURL,
getLogoutURL,
}}
>
{children}
</UserContext.Provider>
);

View File

@@ -1,90 +1,83 @@
jest.mock("../../services/auth");
jest.mock("../../services/timer");
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
import { UserProvider, useUserContext } from "../Contexts/UserContext";
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { UserProvider, useUserContext } from "../Contexts/UserContext";
import auth from "../../services/auth";
import getTimerWorker from "../../services/timer";
const TEST_TOKEN = { accessToken: { jwtToken: "TEST" }};
const TEST_TOKEN = {
idToken: {
jwtToken: "TEST",
payload: {
exp: new Date().getTime() / 1000,
},
},
};
const INVALID_TOKEN_RESPONSE = {
error: "Bad Request Error",
message: "Bad Request Message",
};
const setupRefreshEnv = (refreshResponse, valid) => {
auth.setRefreshResponse(refreshResponse);
auth.setVerifyResponse({ valid });
};
const setupSignInEnv = (refreshResponse, valid) => {
auth.setSignInResponse(refreshResponse);
auth.setVerifyResponse({ valid });
};
const checkBaseResults = (error, fetching, token) => {
expect(screen.getByTestId("error").innerHTML).toEqual(error);
expect(screen.getByTestId("fetching").innerHTML).toEqual(fetching);
expect(screen.getByTestId("token").innerHTML).toEqual(token);
};
const checkTokenResults = (timer, token) => {
expect(timer.start.mock.calls.length).toEqual(1);
expect(timer.onMessage.mock.calls.length).toEqual(1);
expect(timer.stop.mock.calls.length).toEqual(0);
expect(timer.terminate.mock.calls.length).toEqual(0);
if (!localStorage) {
expect(localStorage.getItem("token")).toEqual(token);
localStorage.removeItem("token");
}
};
describe("UseContext", () => {
describe("Signup", () => {
beforeEach(() => {
const TestComp = () => {
const { signUp, error, fetching } = useUserContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<button data-testid="signUpNoEmail" onClick={() => signUp("")}/>
<button data-testid="signUpNoPassword" onClick={() => signUp("test@test.com", "")}/>
<button data-testid="signUpBadConfirm" onClick={() => signUp("test@test.com", "password", "")}/>
<button data-testid="signUp" onClick={() => signUp("test@test.com", "password", "password")}/>
</>
);
};
render(<UserProvider><TestComp /></UserProvider>);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("Error with no email address", () => {
fireEvent.click(screen.getByTestId("signUpNoEmail"));
expect(screen.getByTestId("error").innerHTML).toEqual("Email is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("Error with no password", () => {
fireEvent.click(screen.getByTestId("signUpNoPassword"));
expect(screen.getByTestId("error").innerHTML).toEqual("Password is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("Error with non-matching password", () => {
fireEvent.click(screen.getByTestId("signUpBadConfirm"));
expect(screen.getByTestId("error").innerHTML).toEqual("Passwords do not match");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("No error sign up", async () => {
fireEvent.click(screen.getByTestId("signUp"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("");
});
it("Handle server error", async () => {
auth.setSignUpResponse({ message: "SERVER-ERROR", error: "ERR" });
fireEvent.click(screen.getByTestId("signUp"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("SERVER-ERROR");
auth.setSignUpResponse({});
});
});
describe("Signin", () => {
beforeEach(() => {
const TestComp = () => {
const { signIn, error, token, fetching } = useUserContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<div data-testid="token">{JSON.stringify(token)}</div>
<button data-testid="signInNoEmail" onClick={() => signIn("")}/>
<button data-testid="signInNoPassword" onClick={() => signIn("test@test.com", "")}/>
<button data-testid="signIn" onClick={() => signIn("test@test.com", "password", "password")}/>
<button data-testid="signInNoCode" onClick={() => signIn("")} />
<button
data-testid="signInInvalidCode"
onClick={() => signIn("INVALID_CODE")}
/>
<button data-testid="signIn" onClick={() => signIn("TEST_CODE")} />
</>
);
};
render(<UserProvider><TestComp /></UserProvider>);
render(
<UserProvider>
<TestComp />
</UserProvider>
);
});
afterEach(() => {
@@ -92,43 +85,41 @@ describe("UseContext", () => {
});
it("Initial state", () => {
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
checkBaseResults("", "false", "null");
});
it("Error with no email address", () => {
fireEvent.click(screen.getByTestId("signInNoEmail"));
expect(screen.getByTestId("error").innerHTML).toEqual("Email is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
it("No auth code", () => {
fireEvent.click(screen.getByTestId("signInNoCode"));
checkBaseResults("", "false", "null");
});
it("Error with no password", () => {
fireEvent.click(screen.getByTestId("signInNoPassword"));
expect(screen.getByTestId("error").innerHTML).toEqual("Password is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
it("Invalid auth code", async () => {
setupSignInEnv(INVALID_TOKEN_RESPONSE, false);
fireEvent.click(screen.getByTestId("signInInvalidCode"));
await waitFor(() =>
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
);
checkBaseResults("Sign in error. Bad Request Message", "false", "null");
});
it("No error sign in", async () => {
it("Sign in form", async () => {
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
auth.setSignInResponse(TEST_TOKEN);
fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("token").innerHTML).toEqual(TOKEN_STRING);
if (!localStorage) return;
expect(localStorage.getItem("token")).toEqual(TOKEN_STRING);
localStorage.removeItem("token");
});
const timer = getTimerWorker();
setupSignInEnv(TEST_TOKEN, true);
it("Handle server error", async () => {
auth.setSignInResponse({ message: "SERVER-ERROR", error: "ERR" });
fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("SERVER-ERROR");
auth.setSignUpResponse({});
await waitFor(() =>
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
);
checkBaseResults("", "false", TOKEN_STRING);
checkTokenResults(timer, TOKEN_STRING);
});
});
@@ -141,15 +132,21 @@ describe("UseContext", () => {
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<div data-testid="token">{JSON.stringify(token)}</div>
<button data-testid="signIn" onClick={() => signIn("test@test.com", "password", "password")}/>
<button data-testid="signOut" onClick={() => signOut()}/>
<button data-testid="signIn" onClick={() => signIn("TEST_CODE")} />
<button data-testid="signOut" onClick={() => signOut()} />
</>
);
};
render(<UserProvider><TestComp /></UserProvider>);
render(
<UserProvider>
<TestComp />
</UserProvider>
);
auth.setSignInResponse(TEST_TOKEN);
fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
await waitFor(() =>
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
);
});
afterEach(() => {
@@ -159,11 +156,79 @@ describe("UseContext", () => {
it("Token cleared", () => {
fireEvent.click(screen.getByTestId("signOut"));
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
checkBaseResults("", "false", "null");
if (!localStorage) return;
expect(localStorage.getItem('token')).toBeNull();
})
})
});
expect(localStorage.getItem("token")).toBeNull();
});
});
describe("Refresh", () => {
beforeEach(() => {
const TestComp = () => {
const { refresh, error, token, fetching } = useUserContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<div data-testid="token">{JSON.stringify(token)}</div>
<button data-testid="refreshNoToken" onClick={() => refresh("")} />
<button
data-testid="refreshInvalidToken"
onClick={() => refresh("INVALID_TOKEN")}
/>
<button
data-testid="refreshValidToken"
onClick={() => refresh("TEST_TOKEN")}
/>
</>
);
};
render(
<UserProvider>
<TestComp />
</UserProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
checkBaseResults("", "false", "null");
});
it("No refresh token", () => {
fireEvent.click(screen.getByTestId("refreshNoToken"));
checkBaseResults("Refresh error. Token required", "false", "null");
});
it("Invalid refresh token", async () => {
setupRefreshEnv(INVALID_TOKEN_RESPONSE, false);
fireEvent.click(screen.getByTestId("refreshInvalidToken"));
await waitFor(() =>
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
);
checkBaseResults("Refresh error. Bad Request Message", "false", "null");
});
it("Valid refresh token", async () => {
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
const timer = getTimerWorker();
setupRefreshEnv(TEST_TOKEN, true);
fireEvent.click(screen.getByTestId("refreshValidToken"));
await waitFor(() =>
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
);
checkBaseResults("", "false", TOKEN_STRING);
checkTokenResults(timer, TOKEN_STRING);
});
});
});

View File

@@ -0,0 +1,65 @@
import React, { useContext, useState } from "react";
import api from "../../services/vehicles";
const VehicleContext = React.createContext();
const validateAdd = (vehicle) => {
if (vehicle === null) {
throw new Error("No vehicle data");
}
if (!vehicle.vin || vehicle.vin.length === 0) {
throw new Error("VIN required");
}
if (vehicle.vin.length > 17) {
throw new Error("VIN cannot be larger than 17 characters");
}
};
export const VehicleProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [vehicles, setVehicles] = useState([]);
const getVehicles = async (search, token) => {
try {
setBusy(true);
const result = await api.getVehicles(search, token);
if (result.error) {
setVehicles([]);
throw new Error(`Get vehicles error. ${result.message}`);
} else {
setVehicles(result.data);
}
} finally {
setBusy(false);
}
};
const addVehicle = async (vehicle, token) => {
try {
setBusy(true);
validateAdd(vehicle);
const result = await api.addVehicle(vehicle, token);
if (result.error) throw new Error(`Add vehicle error. ${result.message}`);
return result;
} finally {
setBusy(false);
}
};
return (
<VehicleContext.Provider
value={{
busy,
vehicles,
getVehicles,
addVehicle,
}}
>
{children}
</VehicleContext.Provider>
);
};
export const useVehicleContext = () => useContext(VehicleContext);

View File

@@ -0,0 +1,144 @@
jest.mock("../../services/vehicles");
import {
render,
cleanup,
screen,
fireEvent,
waitFor,
} from "@testing-library/react";
import { VehicleProvider, useVehicleContext } from "./VehicleContext";
import { StatusProvider, useStatusContext } from "./StatusContext";
const checkVehicleResults = (error, busy, vehicles) => {
checkBaseResults(error, busy);
expect(screen.getByTestId("vehicles").innerHTML).toEqual(vehicles);
};
const checkBaseResults = (error, busy) => {
expect(screen.getByTestId("error").innerHTML).toEqual(error);
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
};
describe("VehicleContext", () => {
describe("getVehicles", () => {
beforeEach(() => {
const TestComp = () => {
const { busy, error, vehicles, getVehicles } = useVehicleContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="busy">{busy.toString()}</div>
<div data-testid="vehicles">{JSON.stringify(vehicles)}</div>
<button
data-testid="getVehicles"
onClick={() => getVehicles(null)}
/>
</>
);
};
render(
<VehicleProvider>
<TestComp />
</VehicleProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
checkVehicleResults("", "false", "[]");
});
it("getVehicles", async () => {
fireEvent.click(screen.getByTestId("getVehicles"));
await waitFor(() =>
expect(screen.getByTestId("vehicles").innerHTML).not.toBe("[]")
);
checkVehicleResults("", "false", JSON.stringify(expectedVehicleData));
});
});
describe("AddVehicles", () => {
beforeEach(async () => {
const TestComp = () => {
const { busy, addVehicle } = useVehicleContext();
const { message, setMessage } = useStatusContext();
const add = async (data) => {
try {
await addVehicle(data);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="addVehiclesNull" onClick={() => add(null)} />
<button data-testid="addVehiclesNoVIN" onClick={() => add({})} />
<button
data-testid="addVehicles"
onClick={() => add({ vin: "XXXXXXXXXXX" })}
/>
</>
);
};
render(
<StatusProvider>
<VehicleProvider>
<TestComp />
</VehicleProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
checkBaseResults("", "false");
});
it("addVehiclesNull", async () => {
fireEvent.click(screen.getByTestId("addVehiclesNull"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("No vehicle data", "false");
});
it("addVehiclesNoVIN", async () => {
fireEvent.click(screen.getByTestId("addVehiclesNoVIN"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("VIN required", "false");
});
it("addVehicles", async () => {
fireEvent.click(screen.getByTestId("addVehicles"));
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
);
checkBaseResults("", "false");
});
});
});
const expectedVehicleData = {
data: [
{ vin: "3C4PDCBG0ET127145" },
{ vin: "1G1FP87S3GN100062" },
{ vin: "1HGCG325XYA062256" },
{ vin: "1J4GZ78YXWC160024" },
{ vin: "2C3CCAAG8CH222800" },
{ vin: "KNADM4A39C6028108" },
{ vin: "1G11C5SL9FF153507" },
],
};

View File

@@ -3,19 +3,20 @@ import React from "react";
let uploading = false;
let progress = 0;
let status = null;
let files = null;
export const FileUploadProvider = ({ children }) => {
return (
<div data-testid="mocked-fileuploadprovider">
{children}
</div>
);
return <div data-testid="mocked-fileuploadprovider">{children}</div>;
};
export const useFileUploadContext = () => ({
uploading,
progress,
status,
files,
upload: jest.fn(),
cancel: jest.fn(),
setFiles: jest.fn((value) => {
files = value;
}),
});

View File

@@ -1,25 +1,27 @@
import React from 'react';
import React from "react";
let token = null;
let fetching = false;
let error = null;
let signInResp = {};
let authorizeURL = "https://cognito.com/authorize?redirect=https://example.com/callback";
let logoutURL = "https://cognito.com/logout?redirect=https://example.com/callback";
export const UserProvider = ({ children }) => {
return (
<div data-testid="mocked-userprovider">
{children}
</div>
);
return <div data-testid="mocked-userprovider">{children}</div>;
};
export const useUserContext = () => ({
token,
fetching,
error,
setError: jest.fn(),
signIn: jest.fn(),
signUp: jest.fn(),
signIn: jest.fn(() => signInResp),
signOut: jest.fn(),
getAuthorizeURL: jest.fn(() => authorizeURL),
getLogoutURL: jest.fn(() => logoutURL),
setError: jest.fn((value) => {
error = value;
}),
});
export const setToken = (val) => {

View File

@@ -0,0 +1,24 @@
import React from "react";
let busy = false;
let vehicles = [];
let error = null;
export const VehicleProvider = ({ children }) => {
return <div data-testid="mocked-vehicleprovider">{children}</div>;
};
export const useVehicleContext = () => ({
busy,
vehicles,
getVehicles: jest.fn(() => vehicles),
addVehicle: jest.fn(),
});
export const setBusy = (val) => {
busy = val;
};
export const setVehicles = (val) => {
vehicles = val;
};