Testing Redux Toolkit Query using jest issue with auth - reactjs

Im currently trying to write jest test for my RTKQuery, but I get stuck on the authentication level for the test.
Basically the api Im using is designed to have the token on query param instead of having it on the request header: "https://api/v1/something/meta/?token=userToken"
So when I try to test the api call it shows me the request has been rejected. Does anyone know how to write the test with this case?
here is my RTKQuery endpoint:
// index.ts
export const rootApi = createApi({
reducerPath: "root",
baseQuery: fetchBaseQuery({baseUrl: API_ROOT}),
endpoints: () => ({});
})
// dataEndpoint.ts
const token = getToken(); // Gets the user's token from localStorage after user login
export cosnt apiWithData = rootApi.injectEndpoints({
endpoints: (build) => ({
fetchDataMetaList: build.mutation<DataType, any>({
query: ({offset = 0, size = 20, body}) => ({
// token is passed in for query param
url: `${API_URL}?offset=${offset}&size=${size}&token=${token}`,
method: "POST",
body: body || {}
})
})
})
})
below is my test:
// data.test.tsx
const body = { offset: 0, size: 20, body: {} };
const updateTimeout = 10000;
beforeEach((): void => {
fetchMock.resetMocks();
})
const wrapper: React.FC = ({ children }) => {
const storeRef = setupApiStore(rootApi);
return <Provider store={storeRef.store}>{children}</Provider>
}
describe("useFetchDataMetaListMutation", () => {
it("Success", async () => {
fetchMock.mockResponse(JSON.string(response));
cosnt { result, waitForNextupdate } = renderHook(
() => useFetchDataMetaListMutation(),
{ wrapper }
)
const [fetchDataMetaList, initialResponse] = result.current;
expect(initialResponse.data).toBeUndefined();
expect(initialResponse.isLoading).toBe(false);
act(() => {
void fetchDataMetaList(body);
})
const loadingResponse = result.current[1];
expect(loadingResponse.data).toBeUndefined();
expect(loadingResponse.isLoading).toBe(true);
// Up til this point everything is passing fine
await waitForNextUpdate({ timeout: updateTimeout });
const loadedResponse = result.current[1];
// expect loadedResponse.data to be defined, but returned undefined
// console out put for loaded Response status is 'rejected' with 401 access level
// error code
})
})

Doing a top-level const token means that as soon as that file is loaded, it will retrieve that token from the local store and that it will never be able to update that - so if that file is loaded before the user is logged in, it will be empty. That is pretty much also what happens in your test here.
To be honest, this might be the first time ever that I see a token as part of the url (that is a serious security problem as the token would be shared between users on copy-pasting the url, it's visible in the browser history even after logout etc!).
Unfortunately in that case, you cannot use prepareHeaders, but at least you could instead of the const use a function to get the current token - and if you import that from another file, you could also use jest mocking to just switch out that import.

Related

All my TRPC queries fail with a 500. What is wrong with my setup?

I am new to TRPC and have set up a custom hook in my NextJS app to make queries. This hook is sending out a query to generateRandomWorker but the response always returns a generic 500 error. I am completely stuck until I can figure out this issue.
The hook:
// filepath: src\utilities\hooks\useCreateRandomWorker.ts
type ReturnType = {
createWorker: () => Promise<Worker>,
isCreating: boolean,
}
const useCreateRandomWorker = (): ReturnType => {
const [isCreating, setIsCreating] = useState(false);
const createWorker = async (): Promise<Worker> => {
setIsCreating(true);
const randomWorker: CreateWorker = await client.generateRandomWorker.query(null);
const createdWorker: Worker = await client.createWorker.mutate(randomWorker);
setIsCreating(false);
return createdWorker;
}
return { createWorker, isCreating };
Here is the router. I know the WorkerService calls work because they are returning the proper values when passed into getServerSideProps which directly calls them. WorkerService.generateRandomWorker is synchronous, the others are async.
// filepath: src\server\routers\WorkerAPI.ts
export const WorkerRouter = router({
generateRandomWorker: procedure
.input(z.null()) // <---- I have tried completely omitting `.input` and with a `null` property
.output(PrismaWorkerCreateInputSchema)
.query(() => WorkerService.generateRandomWorker()),
getAllWorkers: procedure
.input(z.null())
.output(z.array(WorkerSchema))
.query(async () => await WorkerService.getAllWorkers()),
createWorker: procedure
.input(PrismaWorkerCreateInputSchema)
.output(WorkerSchema)
.mutation(async ({ input }) => await WorkerService.createWorker(input)),
});
The Next API listener is at filepath: src\pages\api\trpc\[trpc].ts
When the .input is omitted the request URL is /api/trpc/generateRandomWorker?batch=1&input={"0":{"json":null,"meta":{"values":["undefined"]}}} and returns a 500.
When the .input is z.null() the request URL is /api/trpc/generateRandomWorker?batch=1&input={"0":{"json":null}} and returns a 500.
Can anyone help on what I might be missing?
Additional Info
The client declaration.
// filepath: src\utilities\trpc.ts
export const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: `${getBaseUrl() + trpcUrl}`, // "http://localhost:3000/api/trpc"
fetch: async (input, init?) => {
const fetch = getFetch();
return fetch(input, {
...init,
credentials: "include",
})
}
}),
],
transformer: SuperJSON,
});
The init:
// filepath: src\server\trpc.ts
import SuperJSON from "superjson";
import { initTRPC } from "#trpc/server";
export const t = initTRPC.create({
transformer: SuperJSON,
});
export const { router, middleware, procedure, mergeRouters } = t;
Sorry I am not familiar with the vanilla client. But since you're in react you might be interested in some ways you can call a trpc procedure from anywhere while using the react client:
By using the context you can pretty much do anything from anywhere:
const client = trpc.useContext()
const onClick = async () => {
const data = await client.playlist.get.fetch({id})
}
For a known query, you can disable it at declaration and refetch it on demand
const {refetch} = trpc.playlist.get.useQuery({id}, {enabled: false})
const onClick = async () => {
const data = await refetch()
}
If your procedure is a mutation, it's trivial, so maybe you can turn your GET into a POST
const {mutateAsync: getMore} = trpc.playlist.more.useMutation()
const onClick = async () => {
const data = await getMore({id})
}
Answered.
Turns out I was missing the export for the API handler in api/trpc/[trpc].ts

sessionStorage not available immediately after navigate

I am trying to implement an React solution with Strapi as backend where authorization is done using JWT-keys. My login form is implemented using the function below:
const handleLogin = async (e) => {
let responsekey = null
e.preventDefault();
const data = {
identifier: LoginState.username,
password: LoginState.password
}
await http.post(`auth/local`, data).then((response) => {
setAuth({
userid: response.data.user.id,
loggedin: true
})
responsekey = response.data.jwt
setLoginState({...LoginState, success: true});
sessionStorage.setItem('product-authkey', responsekey);
navigate('/profile');
}).catch(function(error) {
let result = ErrorHandlerAPI(error);
setLoginState({...LoginState, errormessage: result, erroroccurred: true});
});
}
The API-handler should return an Axios item which can be used to query the API. That function is also shown below. If no API-key is present it should return an Axios object without one as for some functionality in the site no JWT-key is necessary.
const GetAPI = () => {
let result = null
console.log(sessionStorage.getItem("product-authkey"))
if (sessionStorage.getItem("product-authkey") === null) {
result = axios.create(
{
baseURL: localurl,
headers: {
'Content-type': 'application/json'
}
}
)
} else {
result = axios.create({
baseURL: localurl,
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${sessionStorage.getItem("product-authkey")}`
}
})
}
return result
}
export default GetAPI()
However, once the user is redirected to the profile page (on which an API-call is made which needs an JWT-key), the request fails as there is no key present in the sessionStorage. The console.log also shows 'null'. If I look at the DevTools I do see that the key is there... And if I refresh the profile page the request goes through with the key, so the key and backend are working as they should.
I tried making the GetAPI function to be synchronous and to move the navigate command out of the await part in the handleLogin function, but that didn't help.
Does someone have an idea?
Thanks!
Sincerely,
Jelle
UPDATE:
Seems to work now, but I need to introduce the getAPI in the useEffect hook, I am not sure if that is a good pattern. This is the code of the profile page:
useEffect(() => {
let testapi = GetAPI()
const getMatches = async () => {
const response = await testapi.get(`/profile/${auth.userid}`)
const rawdata = response.data.data
... etc
}, [setMatchState]
export default GetAPI() this is the problematic line. You are running the GetApi function when the module loads. Basically you only get the token when you visit the site and the js files are loaded. Then you keep working with null. When you reload the page it can load the token from the session storage.
The solution is to export the function and call it when you need to make an api call.

onSuccess callback in Plaid Link not updating

I've built a PlaidLink component using react-plaid-link as below. There's no issues when building with the standard way - passing in only public_token and account_id to the request body.
However, when I attempt to pass in stripeUid to the request body, only an empty string (the initial value of the stripeUid state) is passed. This is despite the value of stripeUid being updated and passed in correctly from the parent via props. For some reason stripeUid does not update within the useCallback hook even though the value is in the dependency array.
Any idea why the value is not updating?
function PlaidLink(props) {
const [token, setToken] = useState("");
const { achPayments, stripeUid } = props;
async function createLinkToken() {
const fetchConfig = {
method: "POST",
};
const response = await fetch(
API_URL + "/plaid/create-link-token",
fetchConfig
);
const jsonResponse = await response.json();
const { link_token } = jsonResponse;
setToken(link_token);
}
const onSuccess = useCallback(
(publicToken, metadata) => {
const { account_id } = metadata;
// Exchange a public token for an access one.
async function exchangeTokens() {
const fetchConfig = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
public_token: publicToken,
account_id,
stripeUid,
}),
};
const response = await fetch(
API_URL + "/plaid/exchange-tokens",
fetchConfig
);
const jsonResponse = await response.json();
console.log("Exchange token response:", jsonResponse);
}
exchangeTokens();
}, [stripeUid]
);
const { open, ready } = usePlaidLink({
token,
onSuccess,
});
// get link_token from your server when component mounts
useEffect(() => {
createLinkToken();
}, []);
useEffect(() => {
if (achPayments && ready) {
open();
}
}, [achPayments, ready, open]);
return <div></div>;
}
export default PlaidLink;
I am not familiar with Stripe API but from reading a code I see a possible issue with the code.
Following the chain of events, there is one usePlaidLink and two useEffects. When the component mounts, it createLinkToken in one of the effects and open in the other (assuming it is ready).
However, when stripeUid changes, it doesn't re-fire the effects. So, that's a hint for me.
Next, checking the source of usePlaidLink here: https://github.com/plaid/react-plaid-link/blob/master/src/usePlaidLink.ts gives me an idea: it doesn't do anything when options.onSuccess changes, only when options.token changes. This is their dependency array:
[loading, error, options.token, products]
So it looks like your code is correct as far as effects in react go, but it doesnt't work together because changing the onSuccess doesn't do anything.
How to solve:
make a pull request into the open source library to fix the issue there
inline that library into your code and fix it for yourself
use "keyed components" to unmount and mount the component again when the uid changes instead of updating to work around the issue
some other solution

Reactjs Cannot Update During an Existing State Transition

I'm creating a global function that checks whether the jwt token is expired or not.
I call this function if I'm fetching data from the api to confirm the user but I'm getting the error that I cannot update during an existing state transition and I don't have a clue what it means.
I also notice the the if(Date.now() >= expiredTime) was the one whose causing the problem
const AuthConfig = () => {
const history = useHistory();
let token = JSON.parse(localStorage.getItem("user"))["token"];
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
localStorage.removeItem("user");
history.push("/login");
} else {
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
}
};
I'm not sure if its correct but I call the function like this, since if jwt token is expired it redirect to the login page.
const config = AuthConfig()
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
I updated this peace of code and I could login to the application but when the jwt expires and it redirect to login using history.push I till get the same error. I tried using Redirect but its a little slow and I could still navigate in privateroutes before redirecting me to login
// old
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime)
// change
if (exp < Date.now() / 1000)
i would start from the beginning telling you that if this is a project that is going to production you always must put the auth token check in the backend especially if we talk about jwt authentication.
Otherwise if you have the strict necessity to put it in the React component i would suggest you to handle this with Promises doing something like this:
const config = Promise.all(AuthConfig()).then(()=> productData());
I would even consider to change the productData function to check if the data variable is not null before saving the state that is the reason why the compiler is giving you that error.
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
data && setProduct(data);
};
Finally consider putting this in the backend. Open another question if you need help on the backend too, i'll be glad to help you.
Have a nice day!
I'm still not sure how your code is used within a component context.
Currently your API and setProduct are called regardless whether AuthConfig() returns any value. During this time, you are also calling history.push(), which may be the reason why you encountered the error.
I can recommend you to check config for value before you try to call the API.
const config = AuthConfig()
if (config) {
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
}
I'm assuming that AuthConfig is a hook, since it contains a hook. And that it's consumed in a React component.
Raise the responsibility of redirecting to the consumer and try to express your logic as effects of their dependencies.
const useAuthConfig = ({ onExpire }) => {
let token = JSON.parse(localStorage.getItem("user"))["token"];
const [isExpired, setIsExpired] = useState(!token);
// Only callback once, when the expired flag turns on
useEffect(() => {
if (isExpired) onExpire();
}, [isExpired]);
// Check the token every render
// This doesn't really make sense cause the expired state
// will only update when the parent happens to update (which
// is arbitrary) but w/e
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
setIsExpired(true);
return null;
}
}
// Don't make a new reference to this object every time
const header = useMemo(() => !isExpired
? ({
headers: {
Authorization: `Bearer ${token}`,
},
})
: null, [isExpired, token]);
return header;
};
const Parent = () => {
const history = useHistory();
// Let the caller decide what to do on expiry,
// and let useAuthConfig just worry about the auth config
const config = useAuthConfig({
onExpire: () => {
localStorage.removeItem("user");
history.push("/login");
}
});
const productData = async (config) => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
useEffect(() => {
if (config) {
productData(config);
}
}, [config]);
};

Integrating API middleware with Redux-Thunk

This is stemming off of this SO Question
I am trying to integrate redux-thunk with my API middleware. The current flow of logic is like this:
action dispatched from component this.props.onVerifyLogin();
=>
action goes to action creator which creates an API call to the middleware like so:
// imports
const verifyLoginAC = createAction(API, apiPayloadCreator);
export const verifyLogin = () => dispatch => {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: `${
localStorage.getItem("token")
? localStorage.getItem("token")
: "not_valid_token"
}`
},
onSuccess: result => verifiedLogin(dispatch, result),
onFailure: result => dispatch(failedLogin(result))
});
};
const verifiedLogin = (dispatch, data) => {
console.log("verifiedLogin");
const user = {
...data.user
};
dispatch(setUser(user));
dispatch({
type: IS_LOGGED_IN,
payload: true
});
};
// failedLogin function
const setUser = createAction(SET_USER);
apiPayloadCreator in utils/appUtils:
const noOp = () => ({ type: "NO_OP" });
export const apiPayloadCreator = ({
url = "/",
method = "GET",
onSuccess = noOp,
onFailure = noOp,
label = "",
isAuthenticated = false,
data = null
}) => {
return {
url,
method,
onSuccess,
onFailure,
isAuthenticated,
data,
label
};
};
and then the middleware intercepts and performs the actual API call:
// imports
// axios default config
const api = ({ dispatch }) => next => action => {
next(action);
console.log("IN API");
console.log("Action: ", action);
// this is where I suspect it is failing. It expects an action object
// but is receiving a function (see below for console.log output)
if (action.type !== API) return;
// handle Axios, fire onSuccess/onFailure, etc
The action is created but is a function instead of an action creator (I understand this is intended for redux-thunk). But when my API goes to check action.type it is not API so it returns, never actually doing anything including call the onSuccess function. I have tried to also add redux-thunk before api in the applyMiddleware but then none of my API actions fire. Can someone assist?
Edit:
This is the received data to the API middleware:
ƒ (dispatch) {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: "" + (localStorage.getItem("token") ? localStorage.getItem("token") : "not_valid_toke…
Status Update:
Still unable to get it work properly. It seems like redux-saga has a pretty good following also, should I try that instead?
My API was interferring. I switched to redux-saga and got everything working like so:
/**
* Redux-saga generator that watches for an action of type
* VERIFY_LOGIN, and then runs the verifyLogin generator
*/
export function* watchVerifyLogin() {
yield takeEvery(VERIFY_LOGIN, verifyLogin);
}
/**
* Redux-saga generator that is called by watchVerifyLogin and queries the
* api to verify that the current token in localStorage is still valid.
* IF SO: SET loggedIn = true, and user = response.data.user
* IF NOT: SET loggedIn = false, and user = {} (blank object}
*/
export function* verifyLogin() {
try {
apiStart(VERIFY_LOGIN);
const token = yield select(selectToken);
const response = yield call(axios.post, "/verify/", {
// use redux-saga's select method to select the token from the state
token: token
});
yield put(setUser(response.data.user));
yield put(setLoggedIn(true));
apiEnd(VERIFY_LOGIN);
} catch (error) {
apiEnd(VERIFY_LOGIN);
yield put(setLoggedIn(false));
yield put(setUser({})); // SET USER TO BLANK OBJECT
}
}

Resources