I have small application where I am using React-Query, The problem is that when I try to activate onError callback, React Query first time checks onSuccess callback Even when the post request data is incorrect, and throws an error
TypeError: Cannot destructure property 'token' of 'data' as it is undefined.
at Object.onSuccess
But actual API response is {response: null, error: "Unauthorised"}
error: "Unauthorised"
response: null
My problem is that I can't access error response from onError callback, What am I doing wrong?
/**
* /* eslint-disable
*
* #format
*/
import { useMutation } from 'react-query';
import { useNavigate } from 'react-router-dom';
import AuthController from '../../../controller/auth';
import { useToasts } from 'react-toast-notifications';
const Login = () => {
return AuthController.Login('user4', 'wrongpass');
};
export default function useUserLogin(setCheckTokenStatus) {
const { addToast } = useToasts();
const navigate = useNavigate();
return useMutation(Login, {
onSuccess: (data) => {
console.log('succ');
let {
token,
tokenExpiresAt,
user: { firstname }
} = data;
setCheckTokenStatus(token);
localStorage.setItem('idToken', JSON.stringify(token));
localStorage.setItem('expires', JSON.stringify(tokenExpiresAt));
navigate('/dashboard', {
replace: true
});
addToast(`hello ${firstname}!`, { appearance: 'success', autoDismiss: true, autoDismissTimeout: 3000 });
},
onError: (error) => {
console.log(error);
addToast(`error: ${error}`, {
appearance: 'error',
autoDismiss: true,
autoDismissTimeout: 3000
});
}
});
}
/** #format */
//AuthController
import myAPI from '../../config/api/index';
export default class AuthController {
static async Login(mobile, password) {
let data = {
mobile,
password,
role: 1
};
try {
const response = await myAPI.post(`/login`, data);
return response.data;
} catch (err) {
console.log('error occured');
}
}
}
Related
https://codesandbox.io/s/login-authentication-usecontext-66t9t?file=/src/index.js
Here how we can pass token in headers for any other pages in codesandbox link. In my code i have action file like this. im getting my response in localstorage.how can i pass my accesstoken here as headers in this page.
import axios from 'axios';
export const upiAction = {
upi,
};
function upi(user) {
return (dispatch) => {
var data = {
upiId: user.upiId,
accountNumber: user.accountNumber,
};
axios
.post('http://localhost:9091/upiidcreation', data
)
.then((res) => {
console.log("res", (res));
const { data } = res;
alert(JSON.stringify(data.responseDesc));
// window.location.pathname = "./homes";
if (data.responseCode === "00") {
window.location.pathname = "./home"
}
})
.catch(err => {
dispatch(setUserUpiError(err, true));
alert("Please Check With details");
});
};
}
export function setUserUpi(showError) {
return {
type: 'SET_UPI_SUCCESS',
showError: showError,
};
}
export function setUserUpiError(error, showError) {
return {
type: 'SET_UPI_ERROR',
error: error,
showError: showError,
};
}
I have the following ErrorLink set for Apollo Client.
export const errorLink = onError(
({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
notificationService.notify("An Error Occurred");
},
);
I need to test this implementation in a unit test.
I've the following to test Apollo Links
const MockQuery = gql`
query {
foo
}
`;
interface LinkResult<T> {
operation: Operation;
result: FetchResult<T>;
}
async function executeLink<T = ApolloLink>(
linkToTest: ApolloLink,
request: GraphQLRequest = { query: MockQuery },
) {
const linkResult = {} as LinkResult<T>;
return new Promise<LinkResult<T>>((resolve, reject) => {
execute(ApolloLink.from([linkToTest]), request).subscribe(
(result) => {
linkResult.result = result as FetchResult<T>;
},
(error) => {
reject(error);
},
() => {
resolve(linkResult);
},
);
});
}
it('triggers a notification on error', () => {
const testLink = new ApolloLink(() => {
await waitFor(() => expect(notificationSpy).toBeCalledWith('An Error Occurred'))
return null;
});
const link = ApolloLink.from([errorLink, testLink]);
executeLink(link);
});
These unit test work fine for other links like AuthLink where I test whether the auth token was set to the localStorage. But I cannot test the error link because I cannot trigger a GraphQL error.
You can create a mocked terminating link and provide a GraphQL operation result.
E.g.
errorLink.ts:
import { onError } from '#apollo/client/link/error';
type ErrorResponse = any;
export const errorLink = onError(({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
console.log('An Error Occurred');
console.log('graphQLErrors: ', graphQLErrors);
});
errorLink.test.ts:
import { ApolloLink, execute, Observable } from '#apollo/client';
import { gql } from 'apollo-server-express';
import { errorLink } from './errorLink';
const MockQuery = gql`
query {
foo
}
`;
describe('68629868', () => {
test('should pass', (done) => {
expect.assertions(1);
const mockLink = new ApolloLink((operation) =>
Observable.of({
errors: [
{
message: 'resolver blew up',
},
],
} as any),
);
const link = errorLink.concat(mockLink);
execute(link, { query: MockQuery }).subscribe((result) => {
expect(result.errors![0].message).toBe('resolver blew up');
done();
});
});
});
test result:
PASS apollo-graphql-tutorial src/stackoverflow/68629868/errorLink.test.ts (5.02s)
68629868
✓ should pass (14ms)
console.log src/stackoverflow/68629868/errorLink.ts:6
An Error Occurred
console.log src/stackoverflow/68629868/errorLink.ts:7
graphQLErrors: [ { message: 'resolver blew up' } ]
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.067s
package version: #apollo/client#3.3.20
I specifically needed to test handling NetworkError with TypeScript and it was a right pain to figure out, so here's how you can do it:
import {
ApolloLink,
execute,
FetchResult,
from,
gql,
GraphQLRequest,
Observable,
Operation,
} from '#apollo/client'
import { errorLink, notificationService } from './'
interface LinkResult<T> {
operation: Operation
result: FetchResult<T>
}
const MockQuery = gql`
query {
foo
}
`
class NetworkError extends Error {
bodyText
statusCode
result
message
response
constructor(networkErrorProps, ...params) {
super(...params)
const {
name,
bodyText,
statusCode,
result,
message,
response,
} = networkErrorProps
this.name = name
this.bodyText = bodyText
this.statusCode = statusCode
this.result = result
this.message = message
this.response = response
}
}
describe('errorLink', () => {
it('should handle error and send notification', async () => {
const mockLink = new ApolloLink((operation, forward) => {
let fetchResult: FetchResult = {
errors: [], // put GraphQLErrors here
data: null,
}
// Thanks https://stackoverflow.com/a/70936974/21217
let linkResult = Observable.of(fetchResult).map(_ => {
throw new NetworkError({
name: 'ServerParseError',
message: 'Unexpected token',
response: {},
bodyText: '<!DOCTYPE html><html><head></head><body>Error</body></html>',
statusCode: 503,
result: {},
})
})
return linkResult
})
async function executeLink<T = any, U = any>(
dataLink: ApolloLink
) {
const linkResult = {} as LinkResult<T>
return new Promise<LinkResult<T>>((resolve, reject) => {
execute(from([errorLink, dataLink]), {
query: MockQuery,
}).subscribe(
result => {
// We don't care
},
error => {
// We can resolve here to skip having a try / catch around the await below
resolve(linkResult)
},
)
})
}
const notificationSpy = jest.spyOn(notificationService, 'notify')
await executeLink(mockLink)
expect(notificationSpy).toHaveBeenCalledWith('An Error Occurred')
})
})
Code under test
// imports
const router = express.Router()
// This is what needs to be mocked
const client = new AwesomeGraphQLClient({
endpoint: process.env.GRAPHCMS_URL || '',
fetch,
fetchOptions: {
headers: {
authorization: `Bearer ${process.env.GRAPHCMS_TOKEN}`
}
}
})
interface LoginRequest {
email: string
password: string
}
router.post(
'/login',
async (req: Request<{}, {}, LoginRequest>, res: Response) => {
try {
const JWT_SECRET = getEnvironment('JWT_SECRET')
const { email, password } = req.body
if (!email || !password) {
res.status(400).json({
message: 'auth.provide.credentials',
full: 'You should provide an email and password'
})
return
}
if (!JWT_SECRET) {
res.status(500).json({
message: 'auth.secret.not.found',
full: 'Secret not found'
})
// TODO error logging
return
}
const { appUsers } = await client.request<
GetUserByEmailResponse,
GetUserByEmailVariables
>(getUserByEmailQuery, {
email
})
if (appUsers.length === 0) {
res.status(404).json({
message: 'auth.wrong.credentials',
full: 'You provided wrong credentials'
})
return
}
const user = appUsers[0]
const result: boolean = await bcrypt.compare(password, user.password)
if (result) {
var token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET)
res.status(200).json({
token
})
return
}
res.status(200).json({
message: 'auth.wrong.credentials',
full: 'You provided wrong credentials in the end'
})
} catch (e) {
console.log('E', e)
const error: ErrorObject = handleError(e)
res.status(error.code).json(error)
}
}
)
Tests for code above
import request from 'supertest'
import app from '../../../app'
import { mocked } from 'ts-jest/utils'
import { compare } from 'bcrypt'
import { AwesomeGraphQLClient } from 'awesome-graphql-client'
const mockRequestFn = jest.fn().mockReturnValue({
appUsers: [
{
id: 'tests'
}
]
})
jest.mock('awesome-graphql-client', () => ({
AwesomeGraphQLClient: jest.fn().mockImplementation(() => ({
request: mockRequestFn
}))
}))
I am trying to mock a method on a non default exported class from Awesome GraphQL. I also want to spy on this method, so I created a separate jest.fn() with a return value. The problem is that request is not a function: TypeError: client.request is not a function.
How can I mock and spy on the method of a mocked non default exported class?
SOLUTION
Managed to find a workaround. Make the method a function that returns the called mockRequest. This way you can spy on AwesomeGraphQLClient.request with mockRequest.toHaveBeenCalledTimes(x).
let mockRequest = jest.fn().mockReturnValue({
appUsers: [
{
id: 'tests'
}
]
})
jest.mock('awesome-graphql-client', () => {
return {
AwesomeGraphQLClient: jest.fn().mockImplementation(() => {
return {
request: () => mockRequest()
}
})
}
})
So, in useEffect I am fetching an object from the API then I am dispatching response data to the Context reducer and then updating the state. It looks something like this:
export const fetchItem = (id) => request({url: `/items/${id}`, method: 'get'});
...
const {dispatch, singleItem} = useProvider();
useEffect(() => {
const id = getItemIdFromUrl(props);
fetchItem(id).then((response) => {
dispatch(action(response.data.data));
});
}, [props, dispatch]);
I would like to write a good integration test for this. I am using react-testing-library with Jest. I am trying to mock the return value of the fetchItem function and then to check if everything is rendered correctly but constantly getting this warning:
act(() => {
/* fire events that update state */
});
/* assert on the output */
Any chance to do this correctly?
This is how the request method looks like:
import axios from 'axios';
import humps from 'humps';
import {getItem} from './localStorage';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});
api.interceptors.response.use(
(response) => humps.camelizeKeys(response),
(error) => Promise.reject(error.response),
);
api.interceptors.request.use(
(request) => {
request.data = humps.decamelizeKeys(request.data);
return request;
},
(error) => Promise.reject(error.request),
);
export default function request({url, method, headers = {}, data}) {
try {
const token = getItem('token');
headers.Authorization = token;
return api({method, url, headers, data});
} catch (error) {
if (error.status === 500) {
console.log('HANDLE ERROR: ', error);
}
throw error;
}
}
action.js
export function getLoginStatus() {
return async(dispatch) => {
let token = await getOAuthToken();
let success = await verifyToken(token);
if (success == true) {
dispatch(loginStatus(success));
} else {
console.log("Success: False");
console.log("Token mismatch");
}
return success;
}
}
component.js
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
});
}
However, when I write component.js code with async/await like below I get this error:
Possible Unhandled Promise Rejection (id: 0): undefined is not a function (evaluating 'this.props.dispatch(splashAction.getLoginStatus())')
component.js
async componentDidMount() {
let success = await this.props.dispatch(splashAction.getLoginStatus());
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
}
How do I await a getLoginStatus() and then execute the rest of the statements?
Everything works quite well when using .then(). I doubt something is missing in my async/await implementation. trying to figure that out.
The Promise approach
export default function createUser(params) {
const request = axios.post('http://www...', params);
return (dispatch) => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
request.then(success => onSuccess, error => onError);
};
}
The async/await approach
export default function createUser(params) {
return async dispatch => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
try {
const success = await axios.post('http://www...', params);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
}
Referenced from the Medium post explaining Redux with async/await: https://medium.com/#kkomaz/react-to-async-await-553c43f243e2
Remixing Aspen's answer.
import axios from 'axios'
import * as types from './types'
export function fetchUsers () {
return async dispatch => {
try {
const users = await axios
.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => res.data)
dispatch({
type: types.FETCH_USERS,
payload: users,
})
} catch (err) {
dispatch({
type: types.UPDATE_ERRORS,
payload: [
{
code: 735,
message: err.message,
},
],
})
}
}
}
import * as types from '../actions/types'
const initialErrorsState = []
export default (state = initialErrorsState, { type, payload }) => {
switch (type) {
case types.UPDATE_ERRORS:
return payload.map(error => {
return {
code: error.code,
message: error.message,
}
})
default:
return state
}
}
This will allow you to specify an array of errors unique to an action.
Another remix for async await redux/thunk. I just find this a bit more maintainable and readable when coding a Thunk (a function that wraps an expression to delay its evaluation ~ redux-thunk )
actions.js
import axios from 'axios'
export const FETCHING_DATA = 'FETCHING_DATA'
export const SET_SOME_DATA = 'SET_SOME_DATA'
export const myAction = url => {
return dispatch => {
dispatch({
type: FETCHING_DATA,
fetching: true
})
getSomeAsyncData(dispatch, url)
}
}
async function getSomeAsyncData(dispatch, url) {
try {
const data = await axios.get(url).then(res => res.data)
dispatch({
type: SET_SOME_DATA,
data: data
})
} catch (err) {
dispatch({
type: SET_SOME_DATA,
data: null
})
}
dispatch({
type: FETCHING_DATA,
fetching: false
})
}
reducers.js
import { FETCHING_DATA, SET_SOME_DATA } from './actions'
export const fetching = (state = null, action) => {
switch (action.type) {
case FETCHING_DATA:
return action.fetching
default:
return state
}
}
export const data = (state = null, action) => {
switch (action.type) {
case SET_SOME_DATA:
return action.data
default:
return state
}
}
Possible Unhandled Promise Rejection
Seems like you're missing the .catch(error => {}); on your promise. Try this:
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
})
.catch(err => {
console.error(err.getMessage());
}) ;
}
use dispatch(this.props.splashAction.getLoginStatus()) instead this.props.dispatch(splashAction.getLoginStatus())