Am new to javascript and i have been trying to write tests for his code but i have not been able to,we are supposed to use jest.
I have researched for long without getting a solution.
document.getElementById("signup").addEventListener("submit", function(e) {
e.preventDefault();
data = {
username: document.getElementById("username").value,
email: document.getElementById("email").value,
password: document.getElementById("password").value,
confirm_password: document.getElementById("confirmPassword").value,
};
});
signUp = (data) => {
fetch("https://diaryapi-v2.herokuapp.com/mydiary/v1/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(data => {
if (data.message === "Your account was created") {
let msg = data.message;
document.getElementById("white").innerHTML = msg;
window.location.href = "/signin";
} else {
let msg = Object.values(data);
console.log(msg)
document.getElementById("white").innerHTML = msg;
}
})
.catch(error => console.error("Error:", error));
}
Your code does not have any exports so you will need to use require() at the point in your test you want it to run.
It makes network requests and sets the window location so you will want to use Mocks Functions to verify it is working as expected without actually making network requests and setting the window location.
It performs some work asynchronously through the use of callbacks given to then() so you will need to account for that in your test.
Jest uses jsdom to provide a browser-like environment within the unit tests and you can use that to set everything up before running your code with require().
I made a few small modifications to your code noted in the comments below:
code.js
document.getElementById("signup").addEventListener("submit", function (e) {
e.preventDefault();
// add 'const'
const data = {
username: document.getElementById("username").value,
email: document.getElementById("email").value,
password: document.getElementById("password").value,
confirm_password: document.getElementById("confirmPassword").value,
};
signUp(data); // call signUp with the data
});
// add 'const'
const signUp = (data) => {
fetch("https://diaryapi-v2.herokuapp.com/mydiary/v1/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(data => {
if (data.message === "Your account was created") {
let msg = data.message;
document.getElementById("white").innerHTML = msg;
window.location.assign("/signin"); // change this to assign() so it can be mocked
} else {
let msg = Object.values(data);
console.log(msg)
document.getElementById("white").innerHTML = msg;
}
})
.catch(error => console.error("Error:", error));
}
Here is a working test for the above code that you can use as a reference:
code.test.js
describe('code', () => {
let fetchMock;
let assignMock;
beforeEach(() => {
// Jest uses jsdom as the default test environment which emulates
// a browser and provides a document object for the unit tests.
// Initialize the document body with the HTML needed for the tests
document.body.innerHTML += `
<form id="signup">
<input type="text" id="username" value="the username">
<input type="text" id="email" value="the email">
<input type="text" id="password" value="the password">
<input type="text" id="confirmPassword" value="the confirmPassword">
<input type="submit" id="submitbutton">
</form>
<div id="white"></div>
`;
// Create a mock for fetch and provide a mock implementation
// so the unit tests aren't actually making network requests
fetchMock = jest.spyOn(global, 'fetch');
fetchMock.mockImplementation(() => Promise.resolve({
json: () => Promise.resolve({ message: 'Your account was created' })
}));
// Create a mock for window.location.assign()
// so the unit tests aren't actually changing the window location
assignMock = jest.spyOn(window.location, 'assign');
assignMock.mockImplementation(() => {});
// With everything set up, require the code
require('./code');
});
afterEach(() => {
// After each test call mockRestore() to restore the original functions
fetchMock.mockRestore();
assignMock.mockRestore();
// resetModules() resets the module registry in Jest and ensures
// a fresh copy of './code' executes on require()
jest.resetModules();
});
it('should fetch data, change contents of #white, and change the page location on submit', async () => {
// Submit the form
document.getElementById('submitbutton').click();
// Check that fetch was called with the expected arguments
expect(fetchMock).toHaveBeenCalledTimes(1);
const fetchArgs = fetchMock.mock.calls[0];
expect(fetchArgs[0]).toBe('https://diaryapi-v2.herokuapp.com/mydiary/v1/auth/register');
expect(fetchArgs[1]).toEqual({
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username: 'the username',
email: 'the email',
password: 'the password',
confirm_password: 'the confirmPassword',
})
});
// pause synchronous execution of the test for two event loop cycles
// so the callbacks queued by the then()'s within signUp have a chance to run
await Promise.resolve().then();
// Check that window.location.assign() was called with the expected arguments
expect(assignMock).toHaveBeenCalledTimes(1);
expect(assignMock.mock.calls[0][0]).toBe('/signin');
// Check that #white was updated
expect(document.getElementById('white').innerHTML).toBe('Your account was created');
});
});
Related
I am trying to use Auth0's actions for post user registration. When I try to test it via UI, it gives me an error like "Error! API Error. Please contact support if the problem persists" and in the console it only writes "Error: {}". The script I wrote for this action looks something like this:
const https = require('https');
const jsonwebtoken = require('jsonwebtoken');
/**
* #param {Event} event - Details about registration event.
*/
exports.onExecutePostUserRegistration = async (event) => {
const TOKEN_SIGNING_KEY = event.secrets.signingKey
const TOKEN_EXPIRATION_IN_SECONDS = 10
const payload = {
email: event.user.email,
name: event.user.given_name,
surname: event.user.family_name
};
const token = jsonwebtoken.sign(payload, TOKEN_SIGNING_KEY,
{ subject: "postUserRegistration",
expiresIn: TOKEN_EXPIRATION_IN_SECONDS });
console.log("Starting post user registration operations for user with email: ", payload.email);
return new Promise((resolve, reject) => {
const request = https.request(url, options,
(res) => {
if (res.statusCode === 200) {
resolve({statusCode: res.statusCode, headers: res.headers})
} else {
reject({
headers: res.headers,
statusCode: res.statusCode
})
}
res.on('error', reject);
});
request.on("error", function (e) {
console.error(e);
reject({message: {e}});
});
request.on("timeout", function () {
reject({message: "request timeout"});
})
request.end();
});
}
Can you help me about what exactly causes this problem?
In order to understand this problem I tried to assign the Promise to a variable and then see what it returned. The funny part was that it was "pending". It couldn't be "pending" in any way, because it everything was true it would be "resolved", else "rejected" and if there is a timeout/error from the request it would be "rejected".
It turns out that Auth0 has some limitations for actions and the endpoint called was stuck to our firewall which caused a timeout in Auth0 before an HTTPS timeout. As the request was broken halfway, it stayed as "pending".
I'm using Passport JWT and I want to check JWT token validity to perform a logout if it's already expired. I already made it work on page refresh but not on route change (from navbar, for example). I need to find a way to check it every time any route component is re-rendered, and I know it's very likely that I don't need to do it on every single component. I just don't know how.
Here's the code that's working for page refresh:
On Express
authenticated.js
router.route("/").get((req, res) => {
passport.authenticate("jwt", { session: false }, (err, user, info) => {
if (!user) {
res.status(401);
res.send("Unauthorized")
}
})(req, res);
});
module.exports = router;
On React
AuthContext.js
export const AuthContext = createContext();
function AuthProvider({ children }) {
const [isLoaded, setIsLoaded] = usePersistedState(false, "loaded");
const [user, setUser] = usePersistedState(null, "username");
const [isAuthenticated, setIsAuthenticated] = usePersistedState(
false,
"auth"
);
useEffect(() => {
axios
.get("/authenticated")
.then((response) => {
return;
})
.catch((error) => {
setUser("");
setIsAuthenticated(false);
setIsLoaded(true);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
{!isLoaded ? (
<h3>Loading...</h3>
) : (
<AuthContext.Provider
value={{ user, setUser, isAuthenticated, setIsAuthenticated }}
>
{children}
</AuthContext.Provider>
)}
</div>
);
}
export default AuthProvider;
index.js
ReactDOM.render(
<AuthProvider>
<App />
</AuthProvider>,
document.getElementById("root")
);
UPDATE
I tried to get it done by adding this useEffect function on my PrivateOutlet.js, which handles the private routes, but it didn't work:
function PrivateOutlet() {
const authContext = useContext(AuthContext);
const location = useLocation();
useEffect(() => {
axios
.get("/authenticated")
.then((response) => {
return;
})
.catch((err) => {
authContext.setIsAuthenticated(false);
authContext.setUser("");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location]);
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace state={{ path: location.pathname }} />
);
}
export default PrivateOutlet;
The response is not specific to any platform/language. In general, the authentication provider/service is issuing the token to the client. The token meant for accessing the private resoruces upon validating by resouce server. As you're using the JWT token, so a resouce server/service can validate the token without talking to authentication service or without performing a database look-up. Your server side code always need to validate the token with the incoming requests, which you can do by creating an interceptor for protected requests. In short, you can invoke call from client to validate the token whenever required.
it could be a socket call for the checking through a wrapper component. If the socket returns false, then history.push him into a page where the login is not required
The approach listed on the "update" section from the original question was on the right path, however there was some caveats on my code preventing it to work properly.
I forgot to remove the useEffect() from AuthContext.js after moving it to PrivateOutlet.js, and I also modified the dependency array to include location.pathname instead of just location:
PrivateOutlet.js
function PrivateOutlet() {
const authContext = useContext(AuthContext);
const location = useLocation();
useEffect(() => {
axios
.get("/authenticated")
.then((response) => {
return;
})
.catch((err) => {
authContext.setIsAuthenticated(false);
authContext.setUser("");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace state={{ path: location.pathname }} />
);
}
export default PrivateOutlet;
This was a minor change and probably didn't interfere at all on anything, but in authenticated.js on my Express server I just moved back from using a custom callback to the standard Passport callback, allowing it to handle unauthorized calls by itself:
authenticated.js
router
.route("/")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
//do some stuff
res.json();
});
module.exports = router;
I'm doing a React web site, with a node.JS REST Api, and actually there is something that I don't know how to do.
When I'm logging in, my React form make a POST request to my Api (considering it's a School Project so I'm working locally, my React app is on port 3000 and my Api is on port 8080), and when I'm submiting, I'm redirected on the response of my Api.
I'd like to be redirected to a page of my React App and receive the response of my Api (when I'm using res.redirect() I have the React page but not the Api response).
Did somebody know how to do it ?
Here is my React Form Component:
class Form extends Component{
constructor(){
super()
this.location = window.location.href
}
render(){
return(
<form action="http://localhost:8080/login" method="post">
<label for="identifier">Mail:</label>
<br/>
<input type="email" id="identifier" name="identifier"/>
<br/><br/>
<label for="password">Password:</label>
<br/>
<input type="password" id="password" name="password"/>
<input id="locator" name="locator" value={this.location} type="hidden"/>
<br/><br/><br/>
<button type="submit">Se connecter</button>
</form>
)
}
}
And here is my Api login route :
app.post('/login', (req, res) => {
console.log(req.body.identifier);
console.log(req.body.password);
client.connect().then(() => {
let newUser = { identifier: req.body.identifier}
return client.db(`${process.env.MONGODB}`).collection(`${process.env.MONGOCOLLECTION}`)
.findOne(newUser)
.then(
result => {
if(result == null){
console.log("No user found");
return res.status(401).json({ error: 'Utilisateur non trouvé !' });
}else{
console.log("User "+result.name+" "+result.firstName+" found");
if(req.body.password !== cryptr.decrypt(result.password)){
console.log("Mot de passe incorrect !");
return res.status(401).json({ error: 'Mot de passe incorrect !' });
}else{
const token = jwt.sign({
id: result._id,
username: result.identifier
}, "my_secret", { expiresIn: '3 hours' })
console.log(token)
return res.json({ access_token: token })
}
}
},
err => res.status(500).json({ err }),
);
})
.then(() => {
console.log("--------------------------------");
})
res.redirect(req.body.locator)
})
You can use XHR requests to handle API calls to server and fetching back data to render in your React component.
Instead of using action in form,
use
<form onSubmit={this.handleSubmit}>
and define a method which handles the submit logic.
async handleSubmit(e) {
const reponse = await
fetch('http://localhost:8080/login', {
method: 'POST',
mode: 'cors', //as your server is on different origin, your server will also have to enable CORS
body: JSON.stringify(formData) //formData will have to be extracted from your input elements before sending to server
})
const data = response.json() //This is the data from your server which you'll get without the need of redirecing.
}
fetch is the default js library to handle XHR request.
There are others as well like axios.
I have the following component:
const WaitingListComponent = () => {
const [email, setEmail] = useState('')
const onSubmit = (e) => {
e.preventDefault()
axios.post("/api/waiting-list/addToList", {
email: email
})
}
return (
<form className="waiting-list-component-container" onSubmit={onSubmit}>
<h4 className="waiting-list-component-heading">Join our waiting list to get early access</h4>
<p className="waiting-list-component-paragraph">Join our waiting list to get exclusive early access to our platform when we're ready to launch.</p>
<input className="waiting-list-component-input" name="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="janedoe#email.com" />
<GeneralButton type="submit" text="Get access" />
</form>
)
}
This Axios request is getting posted via the following function:
const MailerLite = require('mailerlite-api-v2-node').default;
const mailerLite = MailerLite(process.env.MAILER_API);
module.exports = (req, res) => {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
const email = req.body.email;
mailerLite.addSubscriberToGroup(process.env.MAILER_GROUP, email)
.then(() => {
console.log("Successfully sent new subscriber to MailerLite.");
res.send(JSON.stringify({ success: true }));
})
.catch((err) => {
console.log("There was an error.");
console.log(err);
res.send(JSON.stringify({ success: false }));
});
};
This is a post to a website called 'MailerLite'.
Their documentation is here: https://developers.mailerlite.com/docs/getting-started-with-mailerlite-api
And the package I'm using to post via node is here: https://www.npmjs.com/package/mailerlite-api-v2-node#addsubscribertogroupgroupid-subscriber
I'm attempting to use the 'addSubscriberToGroup' function to add a new subscriber to my group.
However, despite the Axios post successfully going through - as shown in the error message - there is an error being generated each time.
I don't want to post the full error because it's lengthy and it contains the API key, but the final two lines indicate it's an Axios error:
isAxiosError: true
Can anyone point out where I'm going wrong here?
If you need more info, or have any specific questions, please let me know!
The issue is probably that you need to send email as an object. You could do it like this: addSubscriberToGroup('id', { email: email })
Current situation
I am developing nodejs backend server and vue frontend application, which is run under different port(localhost:3000 and localhost:8080). With purpose to enable CORS connection, I configured devServer proxy from vue.config.js file.
vue.config.js
module.exports = {
devServer: {
proxy: {
'/users': {
target: 'http://127.0.0.1:3000/users',
changeOrigin: true,
pathRewrite: {
'^/users':''
}
},
'/tasks': {
target: 'http://127.0.0.1:3000/tasks',
changeOrigin: true,
pathRewrite: {
'^/tasks': ''
}
}
}
},
outputDir: '../backend/public'
}
and technically used cors.js to enable request to backend server, which was implemented by expressjs.
I am sending the request with vue component to retrieve data from backend server. It works properly from fetching data from server, and my goal is to make the same behavior when I reload page. However, whenever I reload same page, it keep showing 401 http response status set by the backend code written by myself.
enter image description here
Research and Trial til now
Before I go on the attempts I have tried, I should introduce mandatory codes to be operated at first. Somehow this is at least explanations in which vuex actions using axios, axios using backend routers eventually.
tasks.module.js
import axios from "axios"
import authHeader from '../../services/auth-header'
export const tasks = {
state: {
tasks: []
},
getters: {
allTasks: (state) => state.tasks
},
actions: {
async fetchTasks({ commit }) {
const response = await axios.get('http://127.0.0.1:3000/tasks', {headers: authHeader()})
commit('setTasks', response.data)
axios.defaults.headers.common['Authorization'] = authHeader()
},
async addTask({ commit }, description) {
const response = await axios.post('http://127.0.0.1:3000/tasks', { description, completed: false}, {headers: authHeader()})
commit('newTask', response.data)
},
async updateTask({ commit }, updTask) {
const response = await axios.patch('http://127.0.0.1:3000/tasks/'+updTask.id, updTask, {headers: authHeader()})
commit('updateTask', response.data)
}
},
mutations: {
setTasks: (state, tasks) => (state.tasks = tasks),
newTask: (state, task) => state.tasks.unshift(task),
updateTask: (state, updTask) => {
let updates = Object.keys(updTask)
updates.forEach((update) => {
state.task[update] = updTask[update]
})
}
}
}
TaskManager.vue
<template>
<div class="container">
<div class="jumbotron">
<h3>Task Manager</h3>
<AddTask/>
<Tasks/>
</div>
</div>
</template>
<script>
import Tasks from './components/Tasks'
import AddTask from './components/AddTask'
export default {
name:'TaskManager',
components: {
Tasks,
AddTask
}
}
</script>
Tasks.vue
<template>
<div>
<div>
<div class="legend">
<span>Double click to mark as complete</span>
<span>
<span class="incomplete-box"></span> = Incomplete
</span>
<span>
<span class="complete-box"></span> = Complete
</span>
</div>
</div>
<div class="tasks">
<div
#dblclick="onDblClick(task)"
v-for="task in allTasks"
:key="task.id"
class="task"
v-bind:class="{'is-completed':task.completed}">
{{task.description}}
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: "Tasks",
methods:{
...mapActions(['fetchTasks', 'updateTask']),
onDblClick(task) {
const updTask = {
id: task._id,
description: task.description,
completed: !task.completed
}
console.log(updTask)
this.updateTask(updTask)
}
},
computed: {
...mapGetters(['allTasks']),
},
created() {
this.fetchTasks()
}
}
Now I need to introduce what I have tried to solve problems
Configuring CORS options
Since this error page didnt show any authorization header which was supposed to set in request header I figured out the way I enabled cors connection and I believe this enables preflight request. Here is what I configured middleware behavior from backend code.
task.js(express router file)
const router = new express.Router()
const auth = require('../middleware/auth')
const cors = require('cors')
const corsOptions = {
origin: 'http://127.0.0.1:3000',
allowedHeaders: 'content-Type, Authorization',
maxAge:3166950
}
router.options(cors(corsOptions))
router.get('/tasks', auth, async (req, res) => {
const match = {}
const sort = {}
if(req.query.completed) {
match.completed = req.query.completed === 'true'
}
if(req.query.sortBy) {
const parts = req.query.sortBy.split('_')
sort[parts[0]] = parts[1] === 'desc' ? -1:1 // bracket notation
}
try {
await req.user.populate({
path: 'tasks',
match,
options: {
limit: parseInt(req.query.limit),
skip: parseInt(req.query.skip),
sort
}
}).execPopulate()
console.log(req.user.tasks)
res.status(200).send(req.user.tasks)
} catch (e) {
res.status(500).send(e)
}
})
module.exports = router
auth.js(middleware)
const jwt = require('jsonwebtoken')
const User = require('../models/user')
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ','')
const decoded = jwt.verify(token, 'thisisnewcourse')
console.log('decoded token passed')
const user = await User.findOne({ _id: decoded._id, 'tokens.token': token})
console.log('user found')
if(!user) {
throw new Error()
}
req.token = token
req.user = user
next()
} catch (error) {
console.log('error caught')
res.status(401).send({error: 'please authenticate'})
}
}
module.exports = auth
Set Authorization header as axios default header after login
auth.module.js(since login works correctly, I am copying only login action part)
actions: {
async login ({ commit }, user){
try {
const response = await axios.post('http://127.0.0.1:3000/users/login', user)
if(response.data.token){
localStorage.setItem('user', JSON.stringify(response.data))
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`
}
commit('loginSuccess', response.data)
} catch (e) {
console.log(e)
}
}
Middleware chaining on the express route(cors, auth)
I have tried two different middleware on the same backend code(task.js)
router.get('/tasks', [cors(corsOptions), auth], async (req, res) => {
// same as previously attached code
}
Now I believe referring to another post with similar issue will help me out however it's about having CORS enabled, not the issue that the header is not sent via either preflight request or other type of requests.
You haven't included the code for authHeader but I assume it just returns the value of the Authorization header.
This bit looks suspicious to me:
async fetchTasks({ commit }) {
const response = await axios.get('http://127.0.0.1:3000/tasks', {headers: authHeader()})
commit('setTasks', response.data)
axios.defaults.headers.common['Authorization'] = authHeader()
},
The final line seems to be trying to set the Authorization header globally so that it will be included on all subsequent axios requests. That's fine but it seems strange not to do that sooner. You have a similar line inside the login action, which makes sense, but I assume that isn't being called when the page is refreshed.
Then there's this bit:
{headers: authHeader()}
If authHeader returns the value of the Authorization header then this won't work. Instead you need:
{headers: { Authorization: authHeader() }}
Ideally you wouldn't need to set any headers here and instead you'd just set the global header before attempting this request.
While it isn't the direct cause of your problem, you seem to have got your wires crossed about CORS. You've configured a proxy, which means you aren't using CORS. The request you're making is to the same origin, so CORS doesn't apply. You don't need to include CORS response headers if you aren't making a cross-origin request. If you do want to make a cross-origin request then don't use the proxy. You should try to mimic your production environment during development, so if you intend to use CORS in production you should use it during development. Otherwise, stick with the proxy.