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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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", "", "");
|
||||
});
|
||||
});
|
||||
|
||||
20
src/components/Contexts/StatusContext.jsx
Normal file
20
src/components/Contexts/StatusContext.jsx
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
65
src/components/Contexts/VehicleContext.jsx
Normal file
65
src/components/Contexts/VehicleContext.jsx
Normal 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);
|
||||
144
src/components/Contexts/VehicleContext.test.jsx
Normal file
144
src/components/Contexts/VehicleContext.test.jsx
Normal 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" },
|
||||
],
|
||||
};
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
24
src/components/Contexts/__mocks__/VehicleContext.jsx
Normal file
24
src/components/Contexts/__mocks__/VehicleContext.jsx
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user