Logout route (#238)

* Add logout route

* Don't render spinner on top of content

* Use logout route instead of onBeforeEnterLogout handler

* Remove onBeforeLogoutRouteEnter test

* Fix warnings in tests
This commit is contained in:
Rijk van Zanten
2020-03-23 14:37:31 -04:00
committed by GitHub
parent 7d1df455fd
commit 847c7a5554
11 changed files with 86 additions and 43 deletions

View File

@@ -1,11 +1,11 @@
<template>
<div id="app">
<transition name="fade">
<div class="hydrating" v-show="hydrating">
<div class="hydrating" v-if="hydrating">
<v-progress-circular indeterminate />
</div>
</transition>
<router-view />
<router-view v-if="!hydrating" />
</div>
</template>

View File

@@ -1,7 +1,7 @@
import CollectionsNavigation from './navigation.vue';
import VueCompositionAPI from '@vue/composition-api';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import useNavigation from '../../compositions/use-navigation';
import * as useNavigation from '../../compositions/use-navigation';
import VList, {
VListItem,
VListItemContent,
@@ -10,8 +10,6 @@ import VList, {
} from '@/components/v-list';
import VIcon from '@/components/v-icon';
jest.mock('../../compositions/use-navigation');
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-list', VList);
@@ -22,16 +20,14 @@ localVue.component('v-list-item-icon', VListItemIcon);
localVue.component('v-icon', VIcon);
describe('Modules / Collections / Components / CollectionsNavigation', () => {
beforeEach(() => {
(useNavigation as jest.Mock).mockImplementation(() => ({
navItems: {
value: []
}
}));
});
it('Uses useNavigation to get navigation links', () => {
jest.spyOn(useNavigation, 'default').mockImplementation(
() =>
({
navItems: []
} as any)
);
shallowMount(CollectionsNavigation, { localVue });
expect(useNavigation).toHaveBeenCalled();
expect(useNavigation.default).toHaveBeenCalled();
});
});

View File

@@ -1,13 +1,7 @@
import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api';
import { Route } from 'vue-router';
import {
onBeforeEach,
onBeforeEnterProjectChooser,
replaceRoutes,
defaultRoutes,
onBeforeEnterLogout
} from './router';
import { onBeforeEach, onBeforeEnterProjectChooser, replaceRoutes, defaultRoutes } from './router';
import api from '@/api';
import * as auth from '@/auth';
import { useProjectsStore } from '@/stores/projects';
@@ -256,17 +250,6 @@ describe('Router', () => {
});
});
describe('onBeforeEnterLogout', () => {
it('Calls logout and redirects to login page', async () => {
const to = { ...route, path: '/my-project/logout', params: { project: 'my-project' } };
const from = route;
const next = jest.fn();
await onBeforeEnterLogout(to, from, next);
expect(auth.logout).toHaveBeenCalled();
expect(next).toHaveBeenCalledWith('/my-project/login');
});
});
describe('replaceRoutes', () => {
it('Calls the handler with the default routes', async () => {
const handler = jest.fn(() => []);

View File

@@ -2,8 +2,9 @@ import VueRouter, { NavigationGuard, RouteConfig } from 'vue-router';
import Debug from '@/routes/debug.vue';
import { useProjectsStore } from '@/stores/projects';
import LoginRoute from '@/routes/login';
import LogoutRoute from '@/routes/logout';
import ProjectChooserRoute from '@/routes/project-chooser';
import { checkAuth, logout } from '@/auth';
import { checkAuth } from '@/auth';
import { hydrate, dehydrate } from '@/hydrate';
import useAppStore from '@/stores/app';
@@ -13,12 +14,6 @@ export const onBeforeEnterProjectChooser: NavigationGuard = (to, from, next) =>
next();
};
export const onBeforeEnterLogout: NavigationGuard = async (to, from, next) => {
const currentProjectKey = to.params.project;
await logout({ navigate: false });
next(`/${currentProjectKey}/login`);
};
export const defaultRoutes: RouteConfig[] = [
{
name: 'project-chooser',
@@ -52,7 +47,7 @@ export const defaultRoutes: RouteConfig[] = [
{
name: 'logout',
path: '/:project/logout',
beforeEnter: onBeforeEnterLogout
component: LogoutRoute
},
/**
* @NOTE

View File

@@ -0,0 +1,4 @@
import LogoutRoute from './logout.vue';
export { LogoutRoute };
export default LogoutRoute;

View File

@@ -0,0 +1,23 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import LogoutRoute from './logout.vue';
import { logout } from '@/auth';
import VCircularProgress from '@/components/v-progress/circular/';
import VueCompositionAPI from '@vue/composition-api';
jest.mock('@/auth');
const localVue = createLocalVue();
localVue.component('v-progress-circular', VCircularProgress);
localVue.use(VueCompositionAPI);
describe('Routes / Logout', () => {
it('Calls logout on mount', async () => {
const component = shallowMount(LogoutRoute, {
localVue
});
await component.vm.$nextTick();
expect(logout).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,27 @@
<template>
<div class="logout">
<v-progress-circular indeterminate />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from '@vue/composition-api';
import { logout } from '@/auth';
export default defineComponent({
setup() {
onMounted(() => {
logout();
});
}
});
</script>
<style lang="scss" scoped>
.logout {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>

View File

@@ -0,0 +1,7 @@
# Logout View
Renders an empty page with a spinner, and logs the user out. Once the user is logged out, it will redirect to login.
## Why a separate route?
If we were to logout on any other route that uses the private view, it would mean that there will be a bunch of errors in the devtools, as a lot of things will be trying to read from the store, which will be cleared. This extra route makes sure we can safely empty the store without any component still reading from it.

View File

@@ -23,7 +23,7 @@ describe('Stores / User', () => {
});
describe('Hydrate', () => {
it('Calls the right endpoint', () => {
it('Calls the right endpoint', async () => {
(api.get as jest.Mock).mockImplementation(() =>
Promise.resolve({
data: {
@@ -36,8 +36,12 @@ describe('Stores / User', () => {
projectsStore.state.currentProjectKey = 'my-project';
const userStore = useUserStore(req);
userStore.hydrate().then(() => {
expect(api.get).toHaveBeenCalledWith('/my-project/users/me');
userStore.hydrate();
expect(api.get).toHaveBeenCalledWith('/my-project/users/me', {
params: {
fields: '*,avatar.data'
}
});
});
});

View File

@@ -1,12 +1,14 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import HeaderBar from './header-bar.vue';
import PortalVue from 'portal-vue';
import VButton from '@/components/v-button';
import VIcon from '@/components/v-icon';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.use(PortalVue);
localVue.component('v-button', VButton);
localVue.component('v-icon', VIcon);

View File

@@ -3,9 +3,11 @@ import VueCompositionAPI from '@vue/composition-api';
import PrivateView from './private-view.vue';
import VOverlay from '@/components/v-overlay';
import VProgressCircular from '@/components/v-progress/circular';
import PortalVue from 'portal-vue';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.use(PortalVue);
localVue.component('v-overlay', VOverlay);
localVue.component('v-progress-circular', VProgressCircular);