diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a7437fe8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +name: Admin CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + - run: npm ci + - run: npm run test:unit diff --git a/package-lock.json b/package-lock.json index e65bdd6d..d28bda59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@egjs/vue3-flicking": "^4.10.6", "@vuelidate/core": "^2.0.0", "@vuelidate/validators": "^2.0.0", - "@webitel/ui-sdk": "^24.4.12", + "@webitel/ui-sdk": "^24.4.17", "axios": "^1.6.8", "deep-copy": "^1.4.2", "vue": "^3.4.21", @@ -1533,9 +1533,9 @@ } }, "node_modules/@webitel/ui-sdk": { - "version": "24.4.12", - "resolved": "https://registry.npmjs.org/@webitel/ui-sdk/-/ui-sdk-24.4.12.tgz", - "integrity": "sha512-cnX9cvp4uafeaXw0J5+fHzMWIJKY2KDuoBfTMFO+kiHQBNnWpwEjZYzNsKcPRNhWV6L9m2geNo0uaj049oKaZQ==", + "version": "24.4.17", + "resolved": "https://registry.npmjs.org/@webitel/ui-sdk/-/ui-sdk-24.4.17.tgz", + "integrity": "sha512-nxFXevRpLcEPLYbVm+51ybI2RpXg+ib3yzTc32MHI+DK0VVY4r4/zUvNXbiVp7g1clpOFYbaLb61Vi8bYrrUxw==", "dependencies": { "@floating-ui/vue": "^1.0.1", "@vuelidate/core": "^2.0.3", @@ -9515,9 +9515,9 @@ } }, "@webitel/ui-sdk": { - "version": "24.4.12", - "resolved": "https://registry.npmjs.org/@webitel/ui-sdk/-/ui-sdk-24.4.12.tgz", - "integrity": "sha512-cnX9cvp4uafeaXw0J5+fHzMWIJKY2KDuoBfTMFO+kiHQBNnWpwEjZYzNsKcPRNhWV6L9m2geNo0uaj049oKaZQ==", + "version": "24.4.17", + "resolved": "https://registry.npmjs.org/@webitel/ui-sdk/-/ui-sdk-24.4.17.tgz", + "integrity": "sha512-nxFXevRpLcEPLYbVm+51ybI2RpXg+ib3yzTc32MHI+DK0VVY4r4/zUvNXbiVp7g1clpOFYbaLb61Vi8bYrrUxw==", "requires": { "@floating-ui/vue": "^1.0.1", "@vuelidate/core": "^2.0.3", diff --git a/package.json b/package.json index d72b5408..2d182404 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@egjs/vue3-flicking": "^4.10.6", "@vuelidate/core": "^2.0.0", "@vuelidate/validators": "^2.0.0", - "@webitel/ui-sdk": "^24.4.12", + "@webitel/ui-sdk": "^24.4.17", "axios": "^1.6.8", "deep-copy": "^1.4.2", "vue": "^3.4.21", @@ -25,15 +25,15 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", + "@vitest/coverage-v8": "^1.4.0", "@vue/compiler-sfc": "^3.4.21", "@vue/test-utils": "2.3.0", - "@vitest/coverage-v8": "^1.4.0", "eslint": "^8.16.0", "eslint-plugin-vue": "^8.7.1", + "happy-dom": "^14.3.1", "sass": "^1.52.1", "tslint": "^6.1.3", "vite": "^5.1.6", - "happy-dom": "^14.3.1", "vite-plugin-node-polyfills": "^0.21.0", "vite-plugin-svg-sprite": "^0.5.1", "vitest": "^1.4.0" diff --git a/src/components/auth/__tests__/the-auth.spec.js b/src/components/auth/__tests__/the-auth.spec.js new file mode 100644 index 00000000..f12f2c11 --- /dev/null +++ b/src/components/auth/__tests__/the-auth.spec.js @@ -0,0 +1,50 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import router from '../../../router/router.js'; +import TheAuth from '../the-auth.vue'; + +describe('TheAuth', () => { + let store; + + beforeEach(() => { + store = createStore({ + modules: { + auth: { + namespaced: true, + }, + appearance: { + namespaced: true, + }, + }, + }); + }); + + it('should render', () => { + const wrapper = shallowMount(TheAuth, { + global: { + plugins: [store, router], + mocks: { + $breakpoint: {}, + }, + }, + }); + expect(wrapper.exists()).toBe(true); + }); + + it('calls "submit auth" action on submit event', () => { + const mock = vi.spyOn(TheAuth.methods, 'submitAuth').mockImplementationOnce(vi.fn()); + + const wrapper = mount(TheAuth, { + global: { + plugins: [store, router], + mocks: { + $breakpoint: {}, + }, + }, + }); + + wrapper.findComponent({ name: 'TheLogin'}).vm.$emit('submit'); + + expect(mock).toHaveBeenCalled(); + }); +}); diff --git a/src/components/auth/register/__tests__/the-register.spec.js b/src/components/auth/register/__tests__/the-register.spec.js new file mode 100644 index 00000000..fc6f192f --- /dev/null +++ b/src/components/auth/register/__tests__/the-register.spec.js @@ -0,0 +1,81 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import TheRegister from '../the-register.vue'; + +describe('TheRegister', () => { + let store; + + beforeEach(() => { + store = createStore({ + modules: { + auth: { + namespaced: true, + }, + }, + }); + }); + + it('should render', () => { + const wrapper = shallowMount(TheRegister, { + global: { + plugins: [store], + }, + }); + expect(wrapper.exists()).toBe(true); + }); + + + + it('"next" from first step calls domain check action', () => { + const mock = vi.spyOn(TheRegister.methods, 'checkDomain') + .mockImplementationOnce(vi.fn()); + const wrapper = mount(TheRegister, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtStepper: false, + TheRegisterFirstStep: false, + }, + }, + }); + wrapper.findComponent({ name: 'TheRegisterFirstStep' }).vm.$emit('next'); + expect(mock).toHaveBeenCalled(); + }); + + it('"next" from second step increments activeStep', () => { + const wrapper = mount(TheRegister, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtStepper: false, + TheRegisterSecondStep: false, + }, + }, + data: () => ({ + activeStep: 2, + }), + }); + wrapper.findComponent({ name: 'TheRegisterSecondStep' }).vm.$emit('next'); + expect(wrapper.vm.activeStep).toBe(3); + }); + + it('"next" from third step emits "submit" event', () => { + const wrapper = mount(TheRegister, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtStepper: false, + TheRegisterThirdStep: false, + }, + }, + data: () => ({ + activeStep: 3, + }), + }); + wrapper.findComponent({ name: 'TheRegisterThirdStep' }).vm.$emit('next'); + expect(wrapper.emitted('submit')).toBeTruthy(); + }); +}); diff --git a/src/components/auth/register/steps/__tests__/the-register-first-step.spec.js b/src/components/auth/register/steps/__tests__/the-register-first-step.spec.js new file mode 100644 index 00000000..3a57c27f --- /dev/null +++ b/src/components/auth/register/steps/__tests__/the-register-first-step.spec.js @@ -0,0 +1,62 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import TheRegisterFirstStep from '../the-register-first-step.vue'; + +const v$ = { + value: { + $touch: vi.fn(), + }, +}; + +describe('TheRegisterFirstStep', () => { + let store; + + beforeEach(() => { + store = createStore({ + modules: { + auth: { + namespaced: true, + }, + }, + }); + }); + + it('should render', () => { + const wrapper = shallowMount(TheRegisterFirstStep, { + global: { + plugins: [store], + }, + data: () => ({ v$ }), + }); + expect(wrapper.exists()).toBe(true); + }); + + it('emits "next" event on "next" btn click', () => { + const wrapper = mount(TheRegisterFirstStep, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtButton: false, + }, + }, + data: () => ({ v$ }), + }); + wrapper.findAllComponents({ name: 'WtButton' }).find((btn) => { + return btn.html().toLocaleLowerCase().includes('next'); + }).vm.$emit('click'); + expect(wrapper.emitted('next')).toBeTruthy(); + }); + + it('emits "login" event on register link click', () => { + const wrapper = mount(TheRegisterFirstStep, { + shallow: true, + global: { + plugins: [store], + }, + data: () => ({ v$ }), + }); + wrapper.find('.auth-form-actions--link').trigger('click'); + expect(wrapper.emitted('login')).toBeTruthy(); + }); +}); diff --git a/src/components/auth/register/steps/__tests__/the-register-second-step.spec.js b/src/components/auth/register/steps/__tests__/the-register-second-step.spec.js new file mode 100644 index 00000000..aabfd948 --- /dev/null +++ b/src/components/auth/register/steps/__tests__/the-register-second-step.spec.js @@ -0,0 +1,67 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import TheRegisterSecondStep from '../the-register-second-step.vue'; + +const v$ = { + value: { + $touch: vi.fn(), + }, +}; + +describe('TheRegisterSecondStep', () => { + let store; + + beforeEach(() => { + store = createStore({ + modules: { + auth: { + namespaced: true, + }, + }, + }); + }); + + it('should render', () => { + const wrapper = shallowMount(TheRegisterSecondStep, { + global: { + plugins: [store], + }, + data: () => ({ v$ }), + }); + expect(wrapper.exists()).toBe(true); + }); + + it('emits "next" event on "next" btn click', () => { + const wrapper = mount(TheRegisterSecondStep, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtButton: false, + }, + }, + data: () => ({ v$ }), + }); + wrapper.findAllComponents({ name: 'WtButton' }).find((btn) => { + return btn.html().toLocaleLowerCase().includes('next'); + }).vm.$emit('click'); + expect(wrapper.emitted('next')).toBeTruthy(); + }); + + it('emits "back" event on back button click', () => { + const wrapper = mount(TheRegisterSecondStep, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtButton: false, + }, + }, + data: () => ({ v$ }), + }); + wrapper.findAllComponents({ name: 'WtButton' }).find((btn) => { + return btn.html().toLocaleLowerCase().includes('back'); + }).vm.$emit('click'); + expect(wrapper.emitted('back')).toBeTruthy(); + }); +}); diff --git a/src/components/auth/register/steps/__tests__/the-register-third-step.spec.js b/src/components/auth/register/steps/__tests__/the-register-third-step.spec.js new file mode 100644 index 00000000..da0d5f95 --- /dev/null +++ b/src/components/auth/register/steps/__tests__/the-register-third-step.spec.js @@ -0,0 +1,66 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import TheRegisterThirdStep from '../the-register-third-step.vue'; + +const v$ = { + value: { + $touch: vi.fn(), + }, +}; + +describe('TheRegisterThirdStep', () => { + let store; + beforeEach(() => { + store = createStore({ + modules: { + auth: { + namespaced: true, + }, + }, + }); + }); + + it('should render', () => { + const wrapper = shallowMount(TheRegisterThirdStep, { + global: { + plugins: [store], + }, + data: () => ({ v$ }), + }); + expect(wrapper.exists()).toBe(true); + }); + + it('emits "next" event on "next" btn click', () => { + const wrapper = mount(TheRegisterThirdStep, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtButton: false, + }, + }, + data: () => ({ v$ }), + }); + wrapper.findAllComponents({ name: 'WtButton' }).find((btn) => { + return btn.html().toLocaleLowerCase().includes('register'); + }).vm.$emit('click'); + expect(wrapper.emitted('next')).toBeTruthy(); + }); + + it('emits "back" event on back button click', () => { + const wrapper = mount(TheRegisterThirdStep, { + shallow: true, + global: { + plugins: [store], + stubs: { + WtButton: false, + }, + }, + data: () => ({ v$ }), + }); + wrapper.findAllComponents({ name: 'WtButton' }).find((btn) => { + return btn.html().toLocaleLowerCase().includes('back'); + }).vm.$emit('click'); + expect(wrapper.emitted('back')).toBeTruthy(); + }); +}); diff --git a/src/store/modules/auth/__tests__/auth.spec.js b/src/store/modules/auth/__tests__/auth.spec.js new file mode 100644 index 00000000..93354f23 --- /dev/null +++ b/src/store/modules/auth/__tests__/auth.spec.js @@ -0,0 +1,140 @@ +import contextMock from '@webitel/ui-sdk/src/tests/mocks/contextMock.js'; +import auth from '../auth.js'; +import authAPI from '../../../../api/auth/auth.js'; +import router from '../../../../router/router.js'; + +describe('auth store', () => { + let context; + + beforeEach(() => { + context = contextMock(vi); + }); + + it('SUBMIT_AUTH action calls LOGIN if "action"="login"', async () => { + await auth.actions.SUBMIT_AUTH(context, 'login'); + expect(context.dispatch).toHaveBeenCalledWith('LOGIN'); + }); + + it('SUBMIT_AUTH action calls REGISTER if "action"="register"', async () => { + await auth.actions.SUBMIT_AUTH(context, 'register'); + expect(context.dispatch).toHaveBeenCalledWith('REGISTER'); + }); + + it('SUBMIT_AUTH action calls ON_AUTH_SUCCESS with token, received from prev action', async () => { + const accessToken = 'token'; + context.dispatch.mockImplementationOnce(() => accessToken); + await auth.actions.SUBMIT_AUTH(context, 'login'); + expect(context.dispatch).toHaveBeenCalledWith('ON_AUTH_SUCCESS', { accessToken }); + }); + + it('LOGIN action calls AuthAPI.login with username, password and domain from state', async () => { + const state = { + username: 'username', + password: 'password', + domain: 'domain', + }; + + context.state = state; + + const mock = vi.spyOn(authAPI, 'login').mockImplementationOnce(vi.fn()); + await auth.actions.LOGIN(context); + expect(mock).toHaveBeenCalledWith(state); + }); + + it('REGISTER action calls AuthAPI.register with data from state', async () => { + const state = { + username: 'username', + password: 'password', + domain: 'domain', + certificate: 'certificate', + }; + + context.state = state; + + const mock = vi.spyOn(authAPI, 'register').mockImplementationOnce(vi.fn()); + await auth.actions.REGISTER(context); + expect(mock).toHaveBeenCalledWith(state); + }); + + it('LOAD_SERVICE_PROVIDERS action calls AuthAPI.loadServiceProviders with domain from state', async () => { + const state = { + domain: 'domain', + }; + + const federation = {}; + + context.state = state; + + const mock = vi.spyOn(authAPI, 'loadServiceProviders').mockImplementationOnce(() => ({ federation })); + await auth.actions.LOAD_SERVICE_PROVIDERS(context); + expect(mock).toHaveBeenCalledWith({ domain: state.domain }); + expect(context.commit).toHaveBeenCalledWith('SET_SERVICE_PROVIDERS', federation); + }); + + it('CHECK_CURRENT_SESSION action calls AuthAPI.checkCurrentSession', async () => { + const accessToken = 'vi'; + const mock = vi.spyOn(authAPI, 'checkCurrentSession') + .mockImplementationOnce(() => accessToken); + await auth.actions.CHECK_CURRENT_SESSION(context); + expect(mock).toHaveBeenCalled(); + expect(context.dispatch).toHaveBeenCalledWith('ON_AUTH_SUCCESS', { accessToken }); + }); + + it('ON_AUTH_SUCCESS action dispatches CACHE_USER_DATA', async () => { + const redirect = '/redirect'; + router.currentRoute.value.query = { redirectTo: redirect }; + + await auth.actions.ON_AUTH_SUCCESS(context, { accessToken: 'token' }); + expect(context.dispatch).toHaveBeenCalledWith('CACHE_USER_DATA'); + }); + + it('ON_AUTH_SUCCESS redirects to route redirect with accessToken', async () => { + window = { location: { href: '' } }; + const accessToken = 'vi'; + + const redirect = '/redirect?query=vi'; + router.currentRoute.value.query = { redirectTo: redirect }; + + await auth.actions.ON_AUTH_SUCCESS(context, { accessToken }); + + expect(window.location.href).toBe(`${redirect}&accessToken=${accessToken}`); + }); + + it('CACHE_USER_DATA caches domain by default', async () => { + const domain = 'domain'; + context.state.domain = domain; + context.state.rememberCredentials = false; + + const mock = vi.spyOn(localStorage, 'setItem').mockImplementationOnce(vi.fn()); + + await auth.actions.CACHE_USER_DATA(context); + expect(mock).toHaveBeenCalledWith('auth/domain', domain); + }); + + it('CACHE_USER_DATA caches username + password + remCreds, if remCreds is true', async () => { + const username = 'vi1'; + const password = 'vi2'; + + context.state = { + username, + password, + rememberCredentials: true, + }; + + const mock = vi.spyOn(localStorage, 'setItem').mockImplementationOnce(vi.fn()); + + await auth.actions.CACHE_USER_DATA(context); + expect(mock).toHaveBeenCalledWith('auth/username', username); + expect(mock).toHaveBeenCalledWith('auth/password', password); + expect(mock).toHaveBeenCalledWith('auth/rememberCredentials', 'true'); + }); + + it('CHECK_DOMAIN action calls AuthAPI.checkDomainExistence with domain from state', async () => { + const domain = 'domain'; + context.state.domain = domain; + + const mock = vi.spyOn(authAPI, 'checkDomainExistence').mockImplementationOnce(vi.fn()); + await auth.actions.CHECK_DOMAIN(context); + expect(mock).toHaveBeenCalledWith(domain); + }); +}); diff --git a/tests/config/config.js b/tests/config/config.js index e51229ec..ba098153 100644 --- a/tests/config/config.js +++ b/tests/config/config.js @@ -5,4 +5,4 @@ import i18n from '../../src/locale/i18n'; config.global.plugins = [WebitelUi, i18n]; -vi.doMock('axios', axiosMock); +vi.doMock('axios', axiosMock());