I have been learning react for a while and have been working on creating a pet project. My friend created a test case which tests out some notification message from a method. This method in turn will use a constant from another class.
Below notification component utilizes a set of props(especially the partner props) passed over from routes.js.
class Notification extends Component {
constructor(props) {
super(props);
this.state = {
orientation: "ltr",
services: {
"applications": [],
"eta": "",
"start": ""
},
statuses: {},
locale_date: new Date(),
modal: {
open: false,
header: null,
desription: null
},
// This shouldn't be hardcoded but there are issues with passing this in as a prop in Routes.js
partner: props.partner
}
this.refreshEndpoints();
}
refreshEndpoints = () => {
const ref = this;
axios
.get(this.state.partner.get_device_status_url)
.then(response => {
var statuses = response.data;
if((typeof statuses) !== 'object') return false;
ref.setState({
statuses: statuses
});
}).catch(error => {
});
}
handleCreateNotification = () => {
const ref = this;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(ref.state.services)
};
adalApiFetch(fetch, this.state.partner.get_endpoint_notifications_banner, options)
.then(response => {
ref.setState({
modal: {
open: true,
header: "Success",
description: "Successfully Created Notification"
}
});
})
.catch(function (error) {
ref.setState({
modal: {
open: true,
header: "Error",
description: "Failed to Create Notification"
}
});
});
}
handleDeleteNotification = () => {
const ref = this;
const options = {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(ref.state.services)
};
adalApiFetch(fetch, this.state.partner.get_endpoint_notifications_banner, options)
.then(response => {
ref.setState({
modal: {
open: true,
header: "Success",
description: "Successfully Deleted Notification"
}
});
})
.catch(function (error) {
ref.setState({
modal: {
open: true,
header: "Error",
description: "Failed to Delete Notification"
}
});
});
}
In routes.js I have route for calling out the above component which passes the props for partner.
<ProtectedNotificationPage orientation={orientation} partner={PartnerOne}/>
ParnerOne.js:
export const get_endpoint_notifications_banner = "<some url>"
export const get_device_status_url = "<some url>"
<class components>
I want to utilize the above const in notification component. And I was able to accomplish that using props.partner inside the state method.
But below test case is failing due to undefined property which is strange. But the notification functionality completely works fine. clearing and adding notification has no issues.
describe('Notification component', () => {
it('handleCreateNotification - Success', async () => {
const wrapper = shallow(<Notification />);
await wrapper.instance().handleCreateNotification();
expect(wrapper.state().modal).toEqual(
{
open: true,
header: "Success",
description: "Successfully Created Notification"
}
);
});
it('handleDeleteNotification', async () => {
const wrapper = shallow(<Notification />);
await wrapper.instance().handleDeleteNotification();
expect(wrapper.state().modal).toEqual(
{
open: true,
header: "Success",
description: "Successfully Deleted Notification"
}
);
});
I apologize for my lack of knowledge.. But this is something I couldn't figure out over tutorials/blogs. And I really appreciate if anyone able to point out the issue or reference for fixing this.
I tried utilizing bind across methods, which is something I thought might fix. But didn't workout. Apart from that I also tried accessing the props directly
like this.props.partner.get_device_status_url.. And still test case were failing.
I would suggest the following:
Importing into Notification.js:
const { get_endpoint_notifications_banner, get_device_status_url } = '<path_to_file>'.
You can now access these variables directly inside Notification.js.
Test case was having some issue. When I passed the partner one as props to my test case. It fixed the issue. It was looking for missing props
Related
So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}
You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}
So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.
I have spent the night looking for solutions to this issue, it seems like a lot of people have it and the best advice is often "just switch to SPA mode", which is not an option for me.
I have JWT for authentication, using the JWTSessions gem for Rails.
On the frontend, I have Nuxt with nuxt-auth, using a custom scheme, and the following authorization middleware:
export default function ({ $auth, route, redirect }) {
const role = $auth.user && $auth.user.role
if (route.meta[0].requiredRole !== role) {
redirect('/login')
}
}
The symptom I have is as follows: if I log in and navigate around restricted pages, everything works as expected. I even have fetchOnServer: false for restricted pages, as I only need SSR for my public ones.
However, once I refresh the page or just navigate directly to a restricted URL, I get immediately redirected to the login page by the middleware. Clearly, the user that's authenticated on the client side is not being authenticated on the server side too.
I have the following relevant files.
nuxt.config.js
...
plugins: [
// ...
{ src: '~/plugins/axios' },
// ...
],
// ...
modules: [
'cookie-universal-nuxt',
'#nuxtjs/axios',
'#nuxtjs/auth'
],
// ...
axios: {
baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example.com/v1' : 'http://localhost:3000/v1',
credentials: true
},
auth: {
strategies: {
jwtSessions: {
_scheme: '~/plugins/auth-jwt-scheme.js',
endpoints: {
login: { url: '/signin', method: 'post', propertyName: 'csrf' },
logout: { url: '/signin', method: 'delete' },
user: { url: '/users/active', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: false
}
},
cookie: {
options: {
maxAge: 64800,
secure: process.env.NODE_ENV === 'production'
}
}
},
auth-jwt-scheme.js
const tokenOptions = {
tokenRequired: true,
tokenType: false,
globalToken: true,
tokenName: 'X-CSRF-TOKEN'
}
export default class LocalScheme {
constructor (auth, options) {
this.$auth = auth
this.name = options._name
this.options = Object.assign({}, tokenOptions, options)
}
_setToken (token) {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
}
}
_clearToken () {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
this.$auth.ctx.app.$axios.setHeader('Authorization', false)
}
}
mounted () {
if (this.options.tokenRequired) {
const token = this.$auth.syncToken(this.name)
this._setToken(token)
}
return this.$auth.fetchUserOnce()
}
async login (endpoint) {
if (!this.options.endpoints.login) {
return
}
await this._logoutLocally()
const result = await this.$auth.request(
endpoint,
this.options.endpoints.login
)
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + result
: result
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async setUserToken (tokenValue) {
await this._logoutLocally()
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + tokenValue
: tokenValue
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async fetchUser (endpoint) {
if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
return
}
if (!this.options.endpoints.user) {
this.$auth.setUser({})
return
}
const user = await this.$auth.requestWith(
this.name,
endpoint,
this.options.endpoints.user
)
this.$auth.setUser(user)
}
async logout (endpoint) {
if (this.options.endpoints.logout) {
await this.$auth
.requestWith(this.name, endpoint, this.options.endpoints.logout)
.catch(() => {})
}
return this._logoutLocally()
}
async _logoutLocally () {
if (this.options.tokenRequired) {
this._clearToken()
}
return await this.$auth.reset()
}
}
axios.js
export default function (context) {
const { app, $axios, redirect } = context
$axios.onResponseError(async (error) => {
const response = error.response
const originalRequest = response.config
const access = app.$cookies.get('jwt_access')
const csrf = originalRequest.headers['X-CSRF-TOKEN']
const credentialed = (process.client && csrf) || (process.server && access)
if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
if (process.server) {
$axios.setHeader('X-CSRF-TOKEN', csrf)
$axios.setHeader('Authorization', access)
}
const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })
if (newToken.data.csrf) {
$axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
$axios.setHeader('Authorization', newToken.data.access)
if (app.$auth) {
app.$auth.setToken('jwt_access', newToken.data.csrf)
app.$auth.syncToken('jwt_access')
}
originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
originalRequest.headers.Authorization = newToken.data.access
if (process.server) {
app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
}
return $axios(originalRequest)
} else {
if (app.$auth) {
app.$auth.logout()
}
redirect(301, '/login')
}
} else {
return Promise.reject(error)
}
})
}
This solution is already heavily inspired by material available under other threads and at this point I am pretty much clueless regarding how to authenticate my users universally across Nuxt. Any help and guidance much appreciated.
In order for You not to lose Your authentication session in the system, You first need to save your JWT token to some storage on the client: localStorage or sessionStorage or as well as token data can be saved in cookies.
For to work of the application will be optimally, You also need to save the token in the store of Nuxt. (Vuex)
If You save Your token only in srore of Nuxt and use only state, then every time You refresh the page, Your token will be reset to zero, since the state will not have time to initialize. Therefore, you are redirected to the page /login.
To prevent this from happening, after you save Your token to some storage, You need to read it and reinitialize it in the special method nuxtServerInit(), in the universal mode his will be work on the server side the very first. (Nuxt2)
Then, accordingly, You use Your token when sending requests to the api server, adding to each request that requires authorization, a header of the Authorization type.
Since Your question is specific to the Nuxt2 version, for this version a working code example using cookies to store the token would be:
/store/auth.js
import jwtDecode from 'jwt-decode'
export const state = () => ({
token: null
})
export const getters = {
isAuthenticated: state => Boolean(state.token),
token: state => state.token
}
export const mutations = {
SET_TOKEN (state, token) {
state.token = token
}
}
export const actions = {
autoLogin ({ dispatch }) {
const token = this.$cookies.get('jwt-token')
if (isJWTValid(token)) {
dispatch('setToken', token)
} else {
dispatch('logout')
}
},
async login ({ commit, dispatch }, formData) {
const { token } = await this.$axios.$post('/api/auth/login', formData, { progress: false })
dispatch('setToken', token)
},
logout ({ commit }) {
this.$axios.setToken(false)
commit('SET_TOKEN', null)
this.$cookies.remove('jwt-token')
},
setToken ({ commit }, token) {
this.$axios.setToken(token, 'Bearer')
commit('SET_TOKEN', token)
this.$cookies.set('jwt-token', token, { path: '/', expires: new Date('2024') })
// <-- above use, for example, moment or add function that will computed date
}
}
/**
* Check valid JWT token.
*
* #param token
* #returns {boolean}
*/
function isJWTValid (token) {
if (!token) {
return false
}
const jwtData = jwtDecode(token) || {}
const expires = jwtData.exp || 0
return new Date().getTime() / 1000 < expires
}
/store/index.js
export const state = () => ({
// ... Your state here
})
export const getters = {
// ... Your getters here
}
export const mutations = {
// ... Your mutations here
}
export const actions = {
nuxtServerInit ({ dispatch }) { // <-- init auth
dispatch('auth/autoLogin')
}
}
/middleware/isGuest.js
export default function ({ store, redirect }) {
if (store.getters['auth/isAuthenticated']) {
redirect('/admin')
}
}
/middleware/auth.js
export default function ({ store, redirect }) {
if (!store.getters['auth/isAuthenticated']) {
redirect('/login')
}
}
/pages/login.vue
<template>
<div>
<!-- Your template here-->
</div>
</template>
<script>
export default {
name: 'Login',
layout: 'empty',
middleware: ['isGuest'], // <-- if the user is authorized, then he should not have access to the page !!!
data () {
return {
controls: {
login: '',
password: ''
},
rules: {
login: [
{ required: true, message: 'login is required', trigger: 'blur' }
],
password: [
{ required: true, message: 'password is required', trigger: 'blur' },
{ min: 6, message: 'minimum 6 length', trigger: 'blur' }
]
}
}
},
head: {
title: 'Login'
},
methods: {
onSubmit () {
this.$refs.form.validate(async (valid) => { // <-- Your validate
if (valid) {
// here for example: on loader
try {
await this.$store.dispatch('auth/login', {
login: this.controls.login,
password: this.controls.password
})
await this.$router.push('/admin')
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
} finally {
// here for example: off loader
}
}
})
}
}
}
</script>
! - You must have the following packages installed:
cookie-universal-nuxt
jsonwebtoken
jwt-decode
I think you will find my answer helpful. If something is not clear, ask!
I'm trying to use the ToastController of Ionic outside an vue instance; i've build an separated actions file which will be loaded inside the vue instance itself which handles a request. During this request it does some validations and i would like to throw a toast while something happened. In the vue instance i could do this.$ionic.toastController.create() which is working fine but in this other file there is no vue instance available so i'd tried to import the ToastController there but am not able to make this work.
Someone who can point me in the right direction with this?
I've already tried few options and searching the internet for this; since ionic 4 with vue.js is still in alpha there is very low support at the moment. I'd also use the #modus/ionic-vue instance which is working better then the original from ionic itself at the moment
The actual code will be called during a this.$store.dispatch(RESERVATION_REQUEST) call see example:
import { ToastController } from '#modus/ionic-vue'
import axios from 'axios'
const state = {
status: '',
classes: {},
}
const getters = {
//
}
const actions = {
[RESERVATION_REQUEST]: ({ commit, dispatch }, data) => {
return new Promise(( resolve, reject ) => {
axios({ url: 'reservation/create', data: { lesson: data.lesson, date: data.date, team: data.team }, method: 'POST' })
.then(response => {
ToastController.create({
duration: 2000,
header: 'Confirmation',
message: 'Success',
position: 'top',
showCloseButton: true,
closeButtonText: 'Ok',
}).then(toast => toast.present());
resolve(response)
})
.catch(error => {
ToastController.create({
duration: 2000,
header: 'failed',
message: error.toString(),
position: 'top',
showCloseButton: true,
closeButtonText: 'Ok',
}).then(toast => toast.present());
reject(error)
});
});
},
}
const mutations = {
//
}
export default {
state,
getters,
actions,
mutations,
}
The above code will be called like this:
toggleReservation(lesson, date) {
const team = this.$store.getters.getCurrentId;
this.$store.dispatch(RESERVATION_REQUEST, { lesson, date, team });
}
Would be nice if someone could help me with this; looking for at for a few days right now having the feeling i'm on the right track but can't find the solution yet.
You can do directly then and catch on the $store.dispatch in your vue instance
store:
const actions = {
[RESERVATION_REQUEST]: ({ commit, dispatch }, data) => {
return new Promise(( resolve, reject ) => {
axios({ url: 'reservation/create', data: { lesson: data.lesson, date: data.date, team: data.team }, method: 'POST' })
.then(response => {
resolve(response)
})
.catch(error => {
reject(error)
});
});
},
}
Vue file:
toggleReservation(lesson, date) {
const team = this.$store.getters.getCurrentId;
this.$store.dispatch(RESERVATION_REQUEST, { lesson, date, team })
.then(response => {
this.$ionic.toastController.create({
duration: 2000,
header: 'Confirmation',
message: 'Success',
position: 'top',
showCloseButton: true,
closeButtonText: 'Ok',
}).then(toast => toast.present());
})
.catch(error => {
this.$ionic.toastController.create({
duration: 2000,
header: 'failed',
message: error.toString(),
position: 'top',
showCloseButton: true,
closeButtonText: 'Ok',
}).then(toast => toast.present());
});
}
PS: axios return a promise so if you want you can do return it directly in the store
const actions = {
[RESERVATION_REQUEST]: ({ commit, dispatch }, data) => {
return axios({ url: 'reservation/create', data: { lesson: data.lesson, date: data.date, team: data.team }, method: 'POST' })
},
}
I'm extremely new when it comes to using VueJS and so I am working on a small App that should list out an Authenticated person's Github repositories.
I'm having trouble when it comes to being able to access or even traverse the Array in the Picture below. I keep getting an error of undefinedif I try userRepos. Please see my code below.
I do apologize for the "Wall of Code". But I thought these javascript code snippets are the most pertinent to the issue.
This is the GitHub repo I am using as the boilerplate for this project. GitHub-Electron-Vue-OAuth
const getAxiosClient = (state) => {
return axios.create({
baseURL: state.server.url, // this is "https://api.github.com
headers: {
'Authorization': 'token ' + state.session.access_token
},
responseType: 'json'
})
}
// Mutation
[types.SET_USER_REPOS](state, repos) {
state.session.repos = repos;
},
// State Object
const state = {
server: {
url: 'http://api.github.com'
},
session: {
access_token: window.localStorage.getItem('access_token'),
ready: false,
authenticated: false,
user: {}
}
};
// Actions
export const getRepos = ({
commit,
state
}) => {
return new Promise((resolve, reject) => {
getAxiosClient(state).get('/user/repos').then(response => {
commit(types.SET_USER_REPOS, response.data)
resolve(response.data)
}, err => {
console.log(err)
reject(err)
})
})
}
export const userRepos = (state) => {
console.log(state.session.repos)
return state.session.repos;
}
<template lang="jade">
.home
span Hello {{ username }}
span {{ userRepos }}
</template>
<script>
export default {
name: 'home',
computed: {
username() {
return this.$store.getters.username;
},
userRepos() {
return this.$store.getters.userRepos;
}
},
// TODO: Push this in router
beforeRouteEnter(to, from, next) {
next(vm => {
if (!vm.$store.getters.isAuthenticated) {
vm.$router.push({
name: 'login'
});
} else {
next();
}
});
}
}
</script>
I'm currently having a lot of trouble running tests on my redux actions. The test passes but I get the following error each time it is ran:
ReferenceError: localStorage is not defined
I also got an error before which was:
ReferenceError: fetch is not defined
I fixed this by using isomorphic-fetch. Anyway I am unsure on how I should configure Mocha to run these front end tests. Any help would be much appreciated.
Mocha test command:
mocha -w test/test_helper.js test/*.spec.js
test_helper.js:
require('babel-register')();
var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;
auth.actions.spec.js
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../client/app/actions/auth'
import * as types from '../client/app/constants/ActionTypes'
import nock from 'nock'
import chai from 'chai'
import sinon from 'sinon'
var expect = chai.expect
import { SERVER_API } from './config'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('auth actions', () => {
afterEach(() => {
nock.cleanAll()
})
it('creates LOGIN_REQUEST and LOGINSUCCESS when correct username and password provided', () => {
nock(SERVER_API)
.post('/login', {
username: 'test',
password: 'password'
})
.reply(200, {
token: 'TOKEN'
});
const expectedActions = [
{
type: types.LOGIN_REQUEST,
isFetching: true,
isAuthenticated: false,
creds: {
username: 'test',
password: 'password'
}
},
{
type: types.LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true,
token: 'TOKEN'
}
]
const INITAL_STATE = {
isFetching: false,
isAuthenticated: false
}
const store = mockStore(INITAL_STATE)
return store.dispatch(actions.loginUser({username:'test',password:'password'}))
.then(() => {
expect(store.getActions()).to.deep.equal(expectedActions)
})
})
})
auth.js
import { push } from 'react-router-redux'
import 'es6-promise'
import fetch from 'isomorphic-fetch'
import {
LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE
} from '../constants/ActionTypes.js'
import { SERVER_PORT } from '../constants/config'
function requestLogin(creds) {
return {
type: LOGIN_REQUEST,
isFetching: true,
isAuthenticated: false,
creds
}
}
function receiveLogin(user) {
return {
type: LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true,
token: user.token
}
}
function loginError(message) {
return {
type: LOGIN_FAILURE,
isFetching: false,
isAuthenticated: false,
message
}
}
export function loginUser(creds) {
let config = {
method: 'POST',
headers: { 'Content-Type':'application/x-www-form-urlencoded' },
body: `username=${creds.username}&password=${creds.password}`
}
return dispatch => {
dispatch(requestLogin(creds))
return fetch('http://localhost:'+SERVER_PORT+'/api/login', config)
.then(response =>
response.json()
.then(user => ({ user, response }))
).then(({ user, response }) => {
if (!response.ok) {
dispatch(loginError(user.message))
return Promise.reject(user)
}
else {
dispatch(receiveLogin(user))
localStorage.setItem('token', user.token) //offending line
dispatch(push('foo'))
}
}).catch(err => console.log("Error: ", err))
}
}
Thanks.
Pretty straightforward error, you can't use localStorage on your mocha tests because window.localStorage isn't defined. There are two ways to fix this. The more "canon" way would be to move that localStorage call from your action because this is a side-effect which is an anti-pattern in redux actions. Instead you should have middleware that catches this action and sets the localStorage.
By doing that, you already eliminate the issue of testing this action.
If, however, you don't know how to do that and don't think it wise, then you can "fake" localStorage by making a global variable at the top of your mocha test file which creates a fake localStorage. I recommend against this one but it's definitely a solution that could work in your case.