diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 99bf2cf7aef9..620ddc3e7349 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2436,6 +2436,7 @@ "label.servicelist": "Services", "label.serviceofferingid": "Compute Offering", "label.serviceofferingname": "Compute Offering", +"label.session.expired": "Session Expired", "label.sessions": "Active client sessions", "label.set.default.nic": "Set default NIC", "label.set.reservation": "Set reservation", @@ -4020,6 +4021,7 @@ "message.select.zone.hint": "This is the type of Zone deployment that you want to use. Basic zone: provides a single Network where each Instance is assigned an IP directly from the Network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering). Advanced zone: For more sophisticated Network topologies. This Network model provides the most flexibility in defining guest Networks and providing custom Network offerings such as firewall, VPN, or load balancer support.", "message.server": "Server : ", "message.server.description": "NFS, iSCSI, or PreSetup: IP address or DNS name of the storage device. VMWare PreSetup: IP address or DNS name of the vCenter server. Linstor: http(s) url of the linstor-controller.", +"message.session.expired": "Your session has expired. Please sign in again to continue.", "message.set.default.nic": "Please confirm that you would like to make this NIC the default for this Instance.", "message.set.default.nic.manual": "Please manually update the default NIC on the Instance now.", "message.setting.updated": "Setting Updated:", diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js index f76a9e8884e2..241de2fd1886 100644 --- a/ui/src/utils/request.js +++ b/ui/src/utils/request.js @@ -95,12 +95,11 @@ const err = (error) => { } else if (originalPath !== '/user/login') { countNotify++ store.commit('SET_COUNT_NOTIFY', countNotify) - notification.error({ + notification.warning({ top: '65px', - message: i18n.global.t('label.unauthorized'), - description: i18n.global.t('message.authorization.failed'), + message: i18n.global.t('label.session.expired'), + description: i18n.global.t('message.session.expired'), key: 'http-401', - duration: 0, onClose: () => { let countNotify = store.getters.countNotify countNotify > 0 ? countNotify-- : countNotify = 0 diff --git a/ui/tests/unit/utils/request.spec.js b/ui/tests/unit/utils/request.spec.js new file mode 100644 index 000000000000..7654b5ab0ea7 --- /dev/null +++ b/ui/tests/unit/utils/request.spec.js @@ -0,0 +1,84 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import notification from 'ant-design-vue/es/notification' +import router from '@/router' +import store from '@/store' +import { axios } from '@/utils/request' + +jest.mock('ant-design-vue/es/notification', () => ({ + error: jest.fn(), + warning: jest.fn(), + warn: jest.fn() +})) +jest.mock('@/router', () => ({ + currentRoute: { value: { path: '/dashboard' } }, + push: jest.fn() +})) +jest.mock('@/store', () => ({ + getters: { countNotify: 0 }, + commit: jest.fn(), + dispatch: jest.fn(() => Promise.resolve()) +})) +jest.mock('@/locales', () => ({ + i18n: { global: { t: (key) => key } } +})) +jest.mock('@/vue-app', () => ({ + vueProps: { $localStorage: { get: () => null } } +})) + +const unauthorizedAdapter = (config) => Promise.reject(Object.assign(new Error('Request failed with status code 401'), { + config, + isAxiosError: true, + response: { + status: 401, + config, + data: { listvirtualmachinesresponse: { errorcode: 401, errortext: 'unable to verify user credentials and/or request signature' } } + } +})) + +const callApi = () => axios.get('/client/api', { + params: { command: 'listVirtualMachines' }, + adapter: unauthorizedAdapter +}).catch(() => {}) + +describe('utils > request.js', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('shows a session expired warning when the session times out', async () => { + router.currentRoute.value.path = '/dashboard' + await callApi() + + expect(notification.error).not.toHaveBeenCalled() + expect(notification.warning).toHaveBeenCalledTimes(1) + const shown = notification.warning.mock.calls[0][0] + expect(shown.message).toBe('label.session.expired') + expect(shown.description).toBe('message.session.expired') + expect(shown.duration).toBeUndefined() + expect(store.dispatch).toHaveBeenCalledWith('Logout') + }) + + it('shows nothing when already on the login page', async () => { + router.currentRoute.value.path = '/user/login' + await callApi() + + expect(notification.error).not.toHaveBeenCalled() + expect(notification.warning).not.toHaveBeenCalled() + }) +})