import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { createMemoryHistory } from 'vue-router' import LoginView from '../../src/views/LoginView.vue' import { createAdminRouter } from '../../src/app/router' import { currentUser } from '../../src/state/useAuth' import { navigation } from '../../src/state/useNavigation' import { settings } from '../../src/state/useSettings' import { navigationFixture, settingsFixture } from '../fixtures/typed' import { API, mockApi, profile, requestsTo, resetState, type Route } from '../helpers' const loginOk = { body: { data: { token_type: 'cookie', expires_in: 900 }, meta: {} } } const invalid = { status: 401, body: { error: { code: 'invalid_credentials', message: 'Invalid', details: {} } } } function routes(overrides: Record = {}): Record { return { [`POST ${API}/auth/login`]: loginOk, [`GET ${API}/auth/me`]: { body: profile }, [`GET ${API}/navigation`]: { body: navigationFixture }, [`GET ${API}/settings`]: { body: settingsFixture.list }, ...overrides, } } async function mountLogin(query: Record = {}, overrides: Record = {}) { const calls = mockApi(routes(overrides)) const router = createAdminRouter(createMemoryHistory()) await router.push({ path: '/login', query }) const wrapper = mount(LoginView, { global: { plugins: [router] }, attachTo: document.body }) await flushPromises() return { wrapper, router, calls } } async function submit(wrapper: ReturnType, login = ' dev ', password = 'secret') { await wrapper.find('input[name="login"]').setValue(login) await wrapper.find('input[name="password"]').setValue(password) await wrapper.find('form').trigger('submit') await flushPromises() } beforeEach(() => { resetState() }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('login view', () => { it('labels both inputs and uses the browser credential autocomplete', async () => { const { wrapper } = await mountLogin() expect(wrapper.find('h1').text()).toBe('Witaj ponownie') const [loginLabel, passwordLabel] = wrapper.findAll('label') expect(loginLabel!.text()).toBe('Login lub e-mail') expect(loginLabel!.find('input').attributes('autocomplete')).toBe('username') expect(passwordLabel!.text()).toBe('Hasło') expect(passwordLabel!.find('input').attributes('type')).toBe('password') expect(passwordLabel!.find('input').attributes('autocomplete')).toBe('current-password') expect(wrapper.find('form').attributes('novalidate')).toBeDefined() expect(wrapper.find('[role="alert"]').exists()).toBe(false) }) it('signs in with the trimmed login, loads navigation and settings, clears the password and redirects', async () => { const { wrapper, router, calls } = await mountLogin({ redirect: '/acme/demo/gadgets' }) await submit(wrapper) const [post] = requestsTo(calls, 'POST', `${API}/auth/login`) expect(await post!.json()).toEqual({ login: 'dev', password: 'secret' }) expect(currentUser.value?.login).toBe('dev') expect(navigation.value).toHaveLength(3) expect(settings.value).toHaveLength(3) expect((wrapper.find('input[name="password"]').element as HTMLInputElement).value).toBe('') expect(router.currentRoute.value.fullPath).toBe('/acme/demo/gadgets') }) it.each(['//evil.example/x', 'https://evil.example/'])('ignores the unsafe redirect %j', async (redirect) => { const { wrapper, router } = await mountLogin({ redirect }) await submit(wrapper) expect(router.currentRoute.value.fullPath).toBe('/acme/demo/widgets') }) it('shows the alert, marks the password invalid and stays on login for bad credentials', async () => { const { wrapper, router, calls } = await mountLogin({}, { [`POST ${API}/auth/login`]: invalid }) await submit(wrapper, 'dev', 'wrong') expect(wrapper.find('[role="alert"]').text()).toBe('Nieprawidłowe dane logowania') expect(wrapper.find('input[name="password"]').attributes('aria-invalid')).toBe('true') expect(requestsTo(calls, 'GET', `${API}/auth/me`)).toHaveLength(0) expect(router.currentRoute.value.name).toBe('login') expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeUndefined() }) it('treats a session the profile call cannot confirm as a failed login', async () => { const { wrapper, router } = await mountLogin({}, { [`GET ${API}/auth/me`]: invalid }) await submit(wrapper) expect(wrapper.find('[role="alert"]').exists()).toBe(true) expect(router.currentRoute.value.name).toBe('login') }) it('still signs in when the settings list cannot be loaded', async () => { const { wrapper, router } = await mountLogin({}, { [`GET ${API}/settings`]: invalid }) await submit(wrapper) expect(settings.value).toEqual([]) expect(router.currentRoute.value.fullPath).toBe('/acme/demo/widgets') }) it('disables the submit button while signing in and ignores a second submit', async () => { let release: (() => void) | null = null const { wrapper, calls } = await mountLogin( {}, { [`POST ${API}/auth/login`]: () => new Promise((resolve) => { release = () => resolve(loginOk) }), }, ) await wrapper.find('input[name="login"]').setValue('dev') await wrapper.find('input[name="password"]').setValue('secret') await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit') expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeDefined() release!() await flushPromises() expect(requestsTo(calls, 'POST', `${API}/auth/login`)).toHaveLength(1) }) })