I'm using jest+nock+jsdom modules to test my React\Redux application.
I need to test this async action function:
export function updateUserPhoto (file, token) {
const data = new FormData()
data.append('file', file)
return dispatch => {
dispatch(userPutPhotoRequest())
return axios({
method: 'PUT',
headers: {
'x-access-token': token
},
data: data,
url: API_URL + '/user/photo'
})
.then(res => dispatch(userPutPhotoSuccess(res.data)))
.catch(err => dispatch(userPutPhotoFilure(err)))
}
}
So i'm using jsdom to provide FormData and File objects into tests:
const {JSDOM} = require('jsdom')
const jsdom = (new JSDOM(''))
global.window = jsdom.window
global.document = jsdom.window.document
global.FormData = jsdom.window.FormData
const File = jsdom.window.File
global.File = jsdom.window.File
And this is the method to test "upload photo" function:
it('creates USER_UPDATE_SUCCESS when updating user photo has been done', () => {
const store = mockStore(Map())
const file = new File([''], 'filename.txt', {
type: 'text/plain',
lastModified: new Date()
})
const expectedFormData = new FormData()
expectedFormData.append('file', file)
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo', expectedFormData)
.reply(200, {body: {}})
const expectedActions = [
{
type: ActionTypes.USER_PUT_PHOTO_REQUEST
},
{
type: ActionTypes.USER_PUT_PHOTO_SUCCESS,
response: {
body: {}
}
}
]
return store.dispatch(actions.updateUserPhoto(file, token))
.then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
})
Where i'm using nock to mock axios requests, redux-mock-store to mock Redux store.
Creating File and FormData objects to compare it with response from axios.
And then i'm calling action function passing file and token as parameters.
In production action function works and dispatch action success fine. But in testing i'm receiving error:
Error: Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream
When i pass into axios empty object as data test passes, so problem in FormData object.
How can i mock FormData object for axios in an appropriate way to make this test work ?
This answer is coming way too late, but I was looking to do something similar and I wanted to post a solution here that someone else might stumble across and find useful.
The main problem here is that nock mocks network requests and not Javascript libraries. FormData is a Javascript object that eventually gets transformed to text when making network requests. By the time the FormData object makes it to nock, its been converted to a string or a Buffer, hence the error you see. nock is unable to use the FormData object for comparison.
You have a few options:
1. Easiest solution
Just don't match against the data in the PUT request. The reason you are mocking is because you don't want a real HTTP request to go out but you want a fake response back. nock only mocks the request once, so if you mock all PUT requests to /user/photo nock will catch it but only for that test:
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo')
.reply(200, {body: {}})
Before you implement the test this way, think about what your test is trying to verify. Are you trying to verify that the file is sent in the HTTP request? If yes, then this is a poor option. Your code could send a completely different file than the one dispatched and still pass this test. If however you have another test to verify the file is being put in the HTTP request properly then this solution might save you some time.
2. Easy solution for getting nock to fail on not matching the request
If you do want the test to fail if your code passed a corrupted or wrong file, then he simplest solution would be to test for the filename. Since your file is empty there is no need to match the content, but we can match on the filename:
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo', /Content-Disposition\s*:\s*form-data\s*;\s*name="file"\s*;\s*filename="filename.txt"/i)
.reply(200, {body: {}})
This should match the simple case where you have one file uploading.
3. Matching the content of form data fields
Say you have additional fields to be added to your request
export function updateUserPhoto (file, tags, token) {
const data = new FormData()
data.append('file', file)
data.append('tags', tags)
...
OR you have fake content in the file that you want to match on
const file = new File(Array.from('file contents'), 'filename.txt', {
type: 'text/plain',
lastModified: new Date()
})
This is where things get a bit complex. Essentially what you need to do is to parse the form data text back into an object and then write your own matching logic.
parse-multipart-data is a fairly simple parser that you could use:
https://www.npmjs.com/package/parse-multipart-data
Using that package your test might look something like this
it('creates USER_UPDATE_SUCCESS when updating user photo has been done', () => {
const store = mockStore(Map())
const file = new File(Array.from('file content'), 'filename.txt', {
type: 'text/plain',
lastModified: new Date()
})
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo', function (body) { /* You cannot use a fat-arrow function since we need to access the request headers */
// Multipart Data has a 'boundary' that works as a delimiter.
// You need to extract that
const boundary = this.headers['content-disposition']
.match(/boundary="([^"]+)"/)[1];
const parts = multipart.Parse(Buffer.from(body),boundary);
// return true to indicate a match
return parts[0].filename === 'filename.txt'
&& parts[0].type === 'text/plain'
&& parts[0].data.toString('utf8') === 'file contents'
&& parts[1].name === 'tags[]'
&& parts[1].data.toString('utf8') === 'tag1'
&& parts[2].name === 'tags[]'
&& parts[2].data.toString('utf8') === 'tag2';
})
.reply(200, {body: {}})
const expectedActions = [
{
type: ActionTypes.USER_PUT_PHOTO_REQUEST
},
{
type: ActionTypes.USER_PUT_PHOTO_SUCCESS,
response: {
body: {}
}
}
]
return store.dispatch(actions.updateUserPhoto(file, ['tag1', 'tag2'], token))
.then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
})
I was dealing with the same issue, the problem was that axios was setting http as the default adapter. And xhr is the one you need.
// axios/lib/defaults.js
function getDefaultAdapter() {
var adapter;
// Only Node.JS has a process variable that is of [[Class]] process
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
}
return adapter;
}
So, setting the xhr adapter explicitly on the axios calls worked for me.
Something like:
export function updateUserPhoto (file, token) {
const data = new FormData()
data.append('file', file)
return dispatch => {
dispatch(userPutPhotoRequest())
return axios({
method: 'PUT',
headers: {
'x-access-token': token
},
adapter: require('axios/lib/adapters/xhr'),
data: data,
url: API_URL + '/user/photo'
})
.then(res => dispatch(userPutPhotoSuccess(res.data)))
.catch(err => dispatch(userPutPhotoFilure(err)))
}
}
Also, I had issues with nock and CORS, so, if you have same issue, you can add access-control-allow-origin header
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
})
.defaultReplyHeaders({ 'access-control-allow-origin': '*' })
.put('/user/photo', expectedFormData)
.reply(200, {body: {}})
Related
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.
Using React 16.12.0. We have a number of fetch calls that resemble
const handleFormSubmit = (e) => {
e.preventDefault()
if (password != passConfirm) {
//handle password doesn't match password confirm on submit
setErrors({passConfirm: ["Must match password"]})
return
}
fetch(REACT_APP_PROXY + '/users/', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': `Token ${sessionStorage.getItem('token')}`},
body: JSON.stringify({first_name, last_name, username, password, email})
}).then((response) => {
if (response.ok) {
sessionStorage.setItem('token', response['Refresh-Token'])
setRedirect(true)
} else {
setErrors(response)
console.log(response)
}
})
Note the section where we have an ".ok" response
sessionStorage.setItem('token', response['Refresh-Token'])
We will have a number of these endpoints where we will want to extract this response header and place it in local storage. Is there a more elegant way of applying a response filter to certain endpoints to implement this behavior as opposed to the way we are doing it now?
I don't think there is a possibility for this using fetch except monkey patching fetch itself:
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
let [url, options] = args;
// you can run any request logic here
const response = await originalFetch(url, options);
// you can run any response logic here
return response;
};
But, there must be someone that already did this, so I stumbled upon this npm library fetch-intercept that you can use to do the similar:
import fetchIntercept from 'fetch-intercept';
const unregister = fetchIntercept.register({
response: function (response) {
// Do something with the response
if (response.ok) {
sessionStorage.setItem('token', response['Refresh-Token'])
}
return response;
},
});
// you can call unregister if you don't want run the interceptor anymore
unregister();
Please be aware of the following: You need to require fetch-intercept before you use fetch the first time.
If you are willing to switch from fetch to axios, you can use interceptors and handle it globally.
import axios from 'axios'
axios.interceptors.response.use(response => {
if (response.headers['Refresh-Token']) {
sessionStorage.setItem('token', response.headers['Refresh-Token'])
}
return response
})
Reference: https://axios-http.com/docs/interceptors
Use a function.
async function makeHttpCall(successCallbk) {
var response = await fetch(...arguments);
if(response.ok) {
sessionStorage.setItem('token', response['Refresh-Token']);
successCallback.call();
} else {
setErrors(response);
console.log(response);
}
}
With the following function, I am able to upload images to S3, but I am unable to open the image when I attempt to retrieve it. The following is a function used in a React application that handles the file using 'react-dropzone':
import axios, { post } from 'axios'
import { Buffer } from 'buffer'
const processImage = ({ file }) => {
const reader = new FileReader()
reader.onload = async () => {
const base64data = reader.result
const bufferdata = Buffer.from(base64data, 'base64')
const generateSignedS3Url = '/.netlify/functions/get-s3-urls'
// Obtains signed URLs from a Netlify Serverless Function
const { data } = await post(generateSignedS3Url, {
clientFilename: file.name,
mimeType: file.type,
}).catch(e => {
console.error('Error in generating signed url ', e)
})
axios({
method: 'PUT',
url: data.putUrl,
data: bufferdata,
headers: { 'Content-Type': file.type, 'Content-Encoding': 'base64' },
}).catch(e => {
console.error(e)
})
}
reader.readAsDataURL(file)
}
The following contains the parameters used to obtain the signed URL for the serverless function invoked above:
const putParams = {
Bucket,
Key,
Expires: 2 * 60,
ContentType: body.mimeType,
ContentEncoding: 'base64',
}
const putUrl = s3.getSignedUrl('putObject', putParams)
Here, body.mimeType would be 'image/jpeg'.
Scanning from similar Stack Overflow questions, I've ensured the following:
The header 'Content-Encoding' is set to 'base64'
I am using a buffer to send the content using the NPM buffer library (this is a client application)
The header 'Content-Type' matches the file that is being sent up (ie for a file "background.jpg", the header is set to 'image/jpeg')
I've also attempted to send without a buffer
I've looked through similar SO questions before submitting. They include 1, 2, 3. There are other ones, but they are for the Node backend.
reader.result has a prefix before the encoded data. As described on MDN:
The blob's result cannot be directly decoded as Base64 without first removing the Data-URL declaration preceding the Base64-encoded data. To retrieve only the Base64 encoded string, first remove data:*/*;base64, from the result.
So you can do something like:
const base64data = reader.result.split(',')[1];
I would like to use react-pdf to display the PDF and develop a printing function for direct printing (like using window.print());
The REST server is developed using Jersey.
The PDF will generate from server and return to React client using Jersey with return type is application/pdf. React client will display the PDF using react-pdf.
I don't want to declare the URL path in "file" because this will retrieve the PDF from server again if the React state changed and triggered re-render. Also, I need to develop a print function to print the PDF displayed (Because the PDF content may change if retrieve the PDF again from server)
Below show my code:
Server:
#Override
#GET
#Path("/pdf")
#Produces(MediaType.APPLICATION_PDF_VALUE)
public Response testPdf() throws Exception {
File file = new File("C:\\Desktop\\test.pdf");
FileInputStream fileInputStream = new FileInputStream(file);
ResponseBuilder response = Response.ok((Object) fileInputStream);
response.type("application/pdf");
response.header("Content-Disposition", "filename=test.pdf");
return response.build();
}
Client
import React, { Component } from 'react';
import { Document, Page } from 'react-pdf';
import axios from 'axios';
class MyApp extends Component {
state = {
numPages: null,
pageNumber: 1,
pdfContent: null
}
componentDidMount(){
var that = this;
axios.get("url\Pdf").then((response) => {
that.setState({pdfContent:response.data});
}).catch((error) => {
console.warn(error);
});
}
onDocumentLoadSuccess = ({ numPages }) => {
this.setState({ numPages });
}
printHandler(){
window.print();
}
render() {
const { pageNumber, numPages } = this.state;
return (
<div>
<Document
file={this.state.pdfContent}
onLoadSuccess={this.onDocumentLoadSuccess}
>
<Page pageNumber={pageNumber} />
</Document>
<p>Page {pageNumber} of {numPages}</p>
<button onClick={() => this.setState(prevState => ({
pageNumber: prevState.pageNumber + 1 }))}>Next page</button>
<button onClick={() => this.setState(prevState => ({
pageNumber: prevState.pageNumber - 1 }))}>Prev Page</button>
<button onClick={this.printHandler}/>
</div>
);
}
}
I want to get the PDF only one time and display the PDF using react-pdf. Also, I want to print the displayed PDF.
I tried to convert the response.data to base64 followed this line because not success: (this will lose the pdf content)
Encode PDF to base64 in ReactJS
Code like:
componentDidMount(){
var that = this;
axios.get("url\Pdf").then((response) => {
let reader = new FileReader();
var file = new Blob([response.data], { type: 'application/pdf' });
reader.onloadend = () => {
that.setState({
base64Pdf:reader.result
});
}
reader.readAsDataURL(file);
}).catch((error) => {
console.warn(error);
});
}
Anyone can give me some suggestion?
Or any better way to reach my goal?
Thanks
Update on receiving the error message from the back-end:
When a request fails, we receive a JSON object from the back-end which contains the error message. The problem is that when we are forcing to receive the response in Blob format: responseType: 'blob' - no matter if the request fails or not, we receive a Blob Object. So, I was thinking about changing the responseType in the function provided from axios: transformResponse, but unfortunately, we do not have access to 'responseType' object, only to the headers. Here: https://github.com/axios/axios/pull/1155 there is an open issue about converting accordingly to responseType, it is still not resolved.
So, my way of resolving this problem is using fetch instead of axios.
Here is an example:
fetch('here.is.your/endpoint', {
method: 'POST', // specifying the method request
body: JSON.stringify(request), // specifying the body
headers: {
"Content-Type": "application/json"
}
}
).then((response) => {
if (response.ok) { // checks if the response is with status 200 (successful)
return response.blob().then(blob => {
const name = 'Report.pdf';
saveAs(blob, name);
});
} else {
return response.json().then((jsonError) => {
this.setState({
modalMessage: jsonError.message // access the error message returned from the back-end
});
});
}
}).catch(function (error) {
this.setState({
modalMessage: "Error in data type received." // general handler
});
});
I hope this helps!
Recently I got a similar use case with the pdf part, my request is Post, but you can make it Get with no problem. So, what is happening:
1) - I am using axios for making a request to the back-end:
2) - request is the object that I am sending, but you will not have such, since you will probably send only id, for example: axios.get('here.is.your/endpoint/id');
3) - I am using: file-saver for saving the file I receive.
The rest of the code should be self-explaining and I also added some comments.
import {saveAs} from "file-saver";
...
axios.post('here.is.your/endpoint', qs.parse(request), {
headers: {
'Content-Type': 'application/json'
},
responseType: 'blob' // here I am forcing to receive data in a Blob Format
})
.then(response => {
if (response.data) {
//Create a Blob from the PDF Stream
const file = new Blob(
[response.data],
{type: 'application/pdf'});
const name = 'Report.pdf';
saveAs(file, name);
} else {
throw new Error("Error in data type received.");
}
})
.catch(error => {
this.setState({
modalMessage: "Here Add Custom Message"
});
});
I cannot get the error message from the back-end still, I will text back if I get some progress on it - for now, I show a custom message.
I hope that helps!
Wish you luck!
I am glad I helped!
I have one more UPDATE FOR RECEIVING THE ERROR MESSAGE
THIS ONE IS VALID ONLY IF YOU RECEIVE TEXT MESSAGE NOT JSON
fetch('here.is.your/endpoint', {
method: 'POST', // specifying the method request
body: JSON.stringify(request), // specifying the body
headers: {
"Content-Type": "application/json"
}
}
).then((response) => {
if (response.ok) { // checks if the response is with status 200 (successful)
return response.blob().then(blob => {
const name = 'Report.pdf';
saveAs(blob, name);
});
} else {
return response.text().then(function (error) {
throw new Error(error); // we should throw an Error with the received error
}
);
}
}).catch(function (error) {
this.setState({
modalMessage: error.message // that way we access the error message
});
});
We are using response.text().then() because of that way we manage to convert it from Promise to text. And it is important to use .then() because the Promise at that moment is resolved and we receive the Promise value. Then we simply throw an Error because we do not have access to the state object.
That is how you get a text from a response.
Can I fetch a txt file with redux-api-middleware? If yes, how? According to the doc the body is set to undefined if not application/JSON compliant, but what I will get is a simple text/plain
Basically what I did was altering fetch action and doing my custom stuff with response data. Example:
fetch: async (...args) => {
const response = await fetch(...args);
if (response.ok && response.status === 200) {
const reader = response.body.getReader();
const type = response.headers.get('content-type');
const filename = filenameExtractor(response.headers.get('content-disposition'));
const { value } = await reader.read();
fileSaver({ value, filename, type });
}
return response;
},
This piece of code reads response contents and pushes them for download to the browser. filenameExtractor and fileSaver are custom functions. This way response is not altered.
Using such approach you can further modify response and create json yourself for data to be able to be saved in redux like:
const response = await fetch(...args);
return new Response(
JSON.stringify({ data: response.body })
);