This is my first Nuxt 3 application. I am using oidc-client-ts for authorization and authentication.
First I created this Plugin
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
export default defineNuxtPlugin(({ nuxtApp }) => {
const userManager = new UserManager({
authority: "https://localhost:7267",
client_id: "application-name",
redirect_uri: "http://localhost:3000/oidc/sign-in-callback.html",
response_type: "code",
scope: "openid profile IdentityServerApi role",
loadUserInfo: true,
post_logout_redirect_uri: "http://localhost:3000",
//silent_redirect_uri:"http://localhost:3000",
userStore: new WebStorageStateStore({ store: window.localStorage }),
});
return {
provide: {
auth: userManager,
},
};
});
Then I created this Pinia store
import { defineStore } from "pinia";
export const useAuthStore = defineStore("authStore", {
state: () => ({
user: null,
loading: true,
}),
actions: {
initialize() {
this.$auth.getUser().then((user) => {
if (user) {
this.state.user = user;
}
});
},
},
});
I have two questions.
How do you call useAuthStore.initialize() after the Plugin is instantiated?
When I try to call this.$auth.getUser() in the pinia store I get a 500 error
Related
I want to create a Mock API Server for my Jest tests so that I can define all my backend endpoints and create responses and authentication checks.
I have managed to set up the server and routes by following some of the source code from Chris Fritz "Vue-Enterprice-boilerplate":
https://github.com/chrisvfritz/vue-enterprise-boilerplate/tree/master/tests/unit
// jest.config.js
const _ = require("lodash");
process.env.MOCK_API_PORT = process.env.MOCK_API_PORT || _.random(9000, 9999);
module.exports = {
preset: "#vue/cli-plugin-unit-jest",
setupFiles: ["./tests/unit/setup"],
globalSetup: "<rootDir>/tests/unit/global-setup",
globalTeardown: "<rootDir>/tests/unit/global-teardown",
testMatch: ["**/(*.)spec.js"],
moduleFileExtensions: ["js", "jsx", "json", "vue"],
transform: {
"^.+\\.vue$": "vue-jest",
"^.+\\.js$": "babel-jest",
".+\\.(css|scss|jpe?g|png|gif|webp|svg|mp4|webm|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf)$":
"jest-transform-stub"
},
transformIgnorePatterns: ["/node_modules/(?!vue-spinner)"],
testURL: process.env.API_BASE_URL || `http://localhost:${process.env.MOCK_API_PORT}`
};
The server runs when the tests starts and I can console log the route files.
I just don't know how the axios call from my Vuex would go with the mock API instead of the real one.
Might need to import axios somewhere in the test to prevent the development URL to be used?
/tests/mock-api/routes/auth.js
const Users = require("../resources/users");
module.exports = app => {
console.log('I can see this during tests!');
app.post("/api/v1/login", async (req, res) => {
console.log("I don't see this..");
await Users.authenticate(req.body)
.then(user => {
res.json(user);
})
.catch(error => {
res.status(401).json({ message: error.message });
});
});
});
// /views/Login.spec.js
import Vue from "vue";
import Vuelidate from "vuelidate";
import Login from "#/views/Login";
import BaseButton from "#/components/Globals/_base-button.vue";
import BaseInput from "#/components/Globals/_base-input.vue";
import BaseLabel from "#/components/Globals/_base-label.vue";
import flushPromises from "flush-promises";
import store from "#/store";
import { shallowMount } from "#vue/test-utils";
Vue.use(Vuelidate);
describe("#/views/Login", () => {
// other tests..
it("redirects to posts on successful login", async () => {
const wrapper = shallowMount(Login, { store, stubs: { BaseInput, BaseButton, BaseLabel } });
wrapper.vm.$v.$touch();
const spyDispatch = jest.spyOn(wrapper.vm.$store, "dispatch");
const username = wrapper.find("#username");
const password = wrapper.find("#password");
username.element.value = "johndoe#email.com";
password.element.value = "passwordz";
username.trigger("input");
password.trigger("input");
await wrapper.find("#submitBtn").trigger("click.prevent");
await wrapper.vm.$nextTick();
await flushPromises();
await expect(spyDispatch).toHaveBeenCalledWith("auth/login", {
username: username.element.value,
password: password.element.value
});
// #TODO add expect for redirect as well
});
// /store/auth.js (vuex)
export const actions = {
async login({ commit }, { username, password }) {
console.log("I see this");
const response = await axios.post("/login",
{ username, password }, { withCredentials: true });
console.log("I don't see this");
// #TODO error handling
if (!response) return;
commit("setUser", { ...response.data.user });
router.push({ name: "Posts" });
},
The login action gets called but I don't get passed the axios.post.
Do I need to import axios somewhere to make sure I get a fresh instance? (Vuex uses one I set the baseURL and headers)
All the other tests and logic works except this.
I am creating an application with Vue and using Vue Test Utils and Jest as a unit testing framework. However, I encountered an issue in testing the scenario when it should show invalid credentials on failed login. I was wondering how to mock the Auth of AWS Amplify. I am not quite sure if I am doing the testing right because I am new to unit testing in frontend.
Login.vue:
import { Auth } from 'aws-amplify'
import { required } from 'vuelidate/lib/validators'
export default {
name: 'loginComponent',
data() {
return {
form: {
email: null,
password: null,
},
authErrMsg: '',
isShowAuthErr: false,
isLoading: false,
}
},
validations: {
form: {
email: { required },
password: { required }
}
},
methods: {
validateState(name) {
const { $dirty, $error } = this.$v.form[name];
return $dirty ? !$error : null;
},
onSubmit() {
this.$v.$touch();
if (this.$v.$invalid) {
return
}
this.isLoading = true
this.isShowAuthErr = false
const { email, password } = this.form
Auth.signIn(email, password).then(() => {
this.isLoading = false
this.$store.dispatch('ACTION_SET_LOGGEDIN_STATUS', true)
this.$router.push({ name: 'home' })
}).catch(() => {
this.isLoading = false
this.authErrMsg = 'Invalid login credentials'
this.$store.dispatch('ACTION_SET_LOGGEDIN_STATUS', false)
this.isShowAuthErr = true
})
}
}
}
Login.spec.js:
import { mount, shallowMount, createLocalVue } from '#vue/test-utils'
import Login from '#/components/Login'
import BootstrapVue from 'bootstrap-vue'
import Vuelidate from 'vuelidate'
import Vuex from 'vuex'
import Auth from '#aws-amplify/auth'
import flushPromises from 'flush-promises'
const localVue = createLocalVue()
localVue.use(BootstrapVue)
localVue.use(Vuelidate)
localVue.use(Vuex)
localVue.use(Auth)
let wrapper
beforeEach(() => {
wrapper = mount(Login, { localVue,
form: {
email: null,
password: null,
}
})
})
describe('Login', () => {
it('should error message on failed login', async () => {
wrapper.find('input[name="email"]').setValue('email#gmail.com')
wrapper.find('input[name="password"]').setValue('123ABC')
wrapper.find("form").trigger("submit.prevent")
await flushPromises()
Auth.signIn = jest.fn().mockImplementation(() => {
throw new Error('Incorrect username or password.')
});
expect(Auth.signIn()).rejects.toThrow()
})
})
Error I got:
TypeError: Cannot read property 'clientMetadata' of undefined
I have the following set up in my nuxt.config.js file:
auth: {
redirect: {
login: '/accounts/login',
logout: '/',
callback: '/accounts/login',
home: '/'
},
strategies: {
local: {
endpoints: {
login: { url: 'http://localhost:8000/api/login2/', method: 'post' },
user: {url: 'http://localhost:8000/api/user/', method: 'get', propertyName: 'user' },
tokenRequired: false,
tokenType: false
}
}
},
localStorage: false,
cookie: true
},
I am using django sessions for my authentication backend, which means that upon a successful login, i will have received a session-id in my response cookie. When i authenticate with nuxt however, i see the cookie in the response, but the cookie is not saved to be used in further requests. Any idea what else i need to be doing?
This is how I handled this, which came from a forum post that I cannot find since. First get rid of nuxt/auth and roll your own with vuex store. You will want two middleware, one to apply to pages you want auth on, and another for the opposite.
This assumes you have a profile route and a login route that returns a user json on successful login.
I'm also writing the user to a cookie called authUser, but that was just for debugging and can be removed if you don't need it.
store/index
import state from "./state";
import * as actions from "./actions";
import * as mutations from "./mutations";
import * as getters from "./getters";
export default {
state,
getters,
mutations,
actions,
modules: {},
};
store/state
export default () => ({
user: null,
isAuthenticated: false,
});
store/actions
export async function nuxtServerInit({ commit }, { _req, res }) {
await this.$axios
.$get("/api/users/profile")
.then((response) => {
commit("setUser", response);
commit("setAuthenticated", true);
})
.catch((error) => {
commit("setErrors", [error]); // not covered in this demo
commit("setUser", null);
commit("setAuthenticated", false);
res.setHeader("Set-Cookie", [
`session=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
`authUser=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
]);
});
}
store/mutations
export const setUser = (state, payload) => (state.user = payload);
export const setAuthenticated = (state, payload) =>
(state.isAuthenticated = payload);
store/getters
export const getUser = (state) => state.user;
export const isAuthenticated = (state) => state.isAuthenticated;
middleware/redirectIfNoUser
export default function ({ app, redirect, _route, _req }) {
if (!app.store.state.user || !app.store.state.isAuthenticated) {
return redirect("/auth/login");
}
}
middleware/redirectIfUser
export default function ({ app, redirect, _req }) {
if (app.store.state.user) {
if (app.store.state.user.roles.includes("customer")) {
return redirect({
name: "panel",
params: { username: app.store.state.user.username },
});
} else if (app.store.state.user.roles.includes("admin")) {
return redirect("/admin/dashboard");
} else {
return redirect({
name: "panel",
});
}
} else {
return redirect("/");
}
}
pages/login- login method
async userLogin() {
if (this.form.username !== "" && this.form.password !== "") {
await this.$axios
.post("/api/auth/login", this.form)
.then((response) => {
this.$store.commit("setUser", response.data);
this.$store.commit("setAuthenticated", true);
this.$cookies.set("authUser", JSON.stringify(response.data), {
maxAge: 60 * 60 * 24 * 7,
});
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect);
}
this.$router.push("/panel");
})
.catch((e) => {
this.$toast
.error("Error logging in", { icon: "error" })
.goAway(800);
The cookie is sent by the server but the client won't read it, until you set the property withCredentials in your client request (about withCredentials read here)
To fix your problem you have to extend your auth config with withCredentials property.
endpoints: {
login: {
url: 'http://localhost:8000/api/login2/',
method: 'post'
withCredentials: true
}
}
Also don't forget to set CORS policies on your server as well to support cookie exchange
Example from ExpressJS
app.use(cors({ credentials: true, origin: "http://localhost:8000" }))
More information about this issue on auth-module github
I'm using vue-test-utils with jest, I have router-link being rendered in a Login Component. I'm passing router to the component as well.
There is data called 'email', on its update forgot link get's updated.
Following unit-test checks that.
it("updates forgot password link correctly", done => {
wrapper.setData({
user: {
email: 'a#a.com',
password: '',
}
});
Vue.nextTick(() => {
expect(wrapper.find('a').element.href).toEqual('/forgot-password/?email=a#a.com');
done();
})
})
I'm creating wrapper using following code:
const wrapper = mount(LoginComponent, {
localVue,
sync: false,
stubs: {
RouterLink: RouterLinkStub,
},
mocks: {
$route: {
path: "/login",
meta: {
signout: false
}
}
}
});
What is the correct way to update component data and then check the re-rendered component ?
Try to use async/await , for ex:
it('blah blah', async () => {
wrapper.setData({
user: {
email: 'a#a.com',
password: '',
}
});
await Vue.nextTick()
expect(wrapper.find('a').element.href).toEqual('/forgot-password/?email=a#a.com')
})
see example here https://vue-test-utils.vuejs.org/guides/testing-async-components.html
I'm trying to set up unit tests for a sample Angular5 app using AngularFire2 (version5) google provider login, My auth service is fairly simple and it looks like this:
let authState = null;
let mockAngularFireAuth: any = {authState: Observable.of(authState)};
#Injectable()
export class AuthService {
loggedIn: boolean;
private user: Observable<firebase.User>;
constructor(
public afAuth: AngularFireAuth
) {
this.user = afAuth.authState;
this.user.subscribe(
(user) => {
if (user) {
this.loggedIn = true;
} else {
this.loggedIn = false;
}
});
}
// --------------------------------- Google Login -----------------------------------
loginWithGoogle() {
// Sign in/up with google provider
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION)
.then(() => {
this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider())
.catch((error) => {
if (error.code === 'auth/account-exists-with-different-credential') {
alert('This email address is already registered');
}
});
});
}
// ------------------------- Checks User Authentication -----------------------
isAuthenticated() {
// returns true if the user is logged in
return this.loggedIn;
}
// --------------------------------- User LogOut -----------------------------------
logOut() {
this.afAuth.auth.signOut()
.then(() => {
this.loggedIn = false;
});
}
}
I want to test my loginWithGoogle() method but I am not sure where to start. So far my auth service spec file looks like this:
describe('AuthService', () => {
let authService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AngularFireDatabaseModule,
AngularFireModule.initializeApp(environment.firebase),
RouterTestingModule
],
providers: [
{provide: AngularFireAuth, useValue: mockAngularFireAuth},
AuthService,
]
});
inject([AuthService], (service: AuthService) => {
authService = service;
})();
});
it('should be defined', () => {
expect(authService).toBeDefined();
});
it('should return true if loggedIn is true', () => {
expect(authService.isAuthenticated()).toBeFalsy();
authService.loggedIn = true;
expect(authService.isAuthenticated()).toBeTruthy();
});
});
Any help would be appreciated.
Well, this is what I did. I mocked the AngularFireAuth and returned the promise with reject or resolve promise to be caught. I am new to jasmine and testing, so feel free to correct me if I am doing something wrong.
it('should return a rejected promise', () => {
authState = {
email: 'lanchanagupta#gmail.com',
password: 'password',
};
mockAngularFireAuth = {
auth: jasmine.createSpyObj('auth', {
'signInWithPopup': Promise.reject({
code: 'auth/account-exists-with-different-credential'
}),
}),
authState: Observable.of(authState)
};
mockAngularFireAuth.auth.signInWithPopup()
.catch((error: { code: string }) => {
expect(error.code).toBe('auth/account-exists-with-different-credential');
});
});
it('should return a resolved promise', () => {
authState = {
email: 'lanchanagupta#gmail.com',
password: 'password',
uid: 'nuDdbfbhTwgkF5C6HN5DWDflpA83'
};
mockAngularFireAuth = {
auth: jasmine.createSpyObj('auth', {
'signInWithPopup': Promise.resolve({
user: authState
}),
})
};
mockAngularFireAuth.auth.signInWithPopup()
.then(data => {
expect(data['user']).toBe(authState);
});
});