@@ -0,0 +1,15 @@ +const Sentry = { + init: jest.fn(), + captureException: jest.fn(), + captureMessage: jest.fn(), + withScope: jest.fn((callback) => { + callback({ + setExtra: jest.fn(), + setTag: jest.fn(), + setUser: jest.fn(), + }); + }), +}; + + +export default Sentry; @@ -0,0 +1,25 @@ +const mockRouter = { + push: jest.fn(() => Promise.resolve()), + replace: jest.fn(() => Promise.resolve()), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(() => Promise.resolve()), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + query: {}, + asPath: '', + pathname: '', + route: '', +} + +const useRouter = jest.fn(() => mockRouter) + +export { useRouter } + +export const setRouter = (overrides: Partial) => { + Object.assign(mockRouter, overrides) +} @@ -0,0 +1,30 @@ +import { jest } from '@jest/globals' + +const mockUseSession = jest.fn() +const mockSignIn = jest.fn() +const mockSignOut = jest.fn() +const mockGetSession = jest.fn() + +const mockSession = { + user: { + name: 'Test User', + email: 'test@example.com', + image: 'https://example.com/test.jpg', + }, + expires: '1', +} + +export const useSession = () => { + return { data: mockSession, status: 'authenticated' } +} + +export const signIn = mockSignIn +export const signOut = mockSignOut +export const getSession = mockGetSession + +export default { + useSession, + signIn, + signOut, + getSession, +} @@ -0,0 +1,3 @@ +const ReactComponent = 'div' + +export default ReactComponent \ No newline at end of file @@ -0,0 +1,27 @@ +import { Modal, usePlatesStore } from '#/features/modals/model' +import { useEffect } from 'react' +import { jestRender } from './render' + +const ModalTester = ({ id, onModal }: { id: string; onModal: (modal: Modal) => void }) => { + const { getModal } = usePlatesStore() + + useEffect(() => { + const modal = getModal(id) + onModal(modal) + }, [id, onModal]) + + return null +} + +export async function getModalTest(id: string): Promise { + return await new Promise((resolve) => { + jestRender( + { + resolve(m) + }} + /> + ) + }) +} @@ -0,0 +1,9 @@ +import { store } from '#/app/store/store' +import { render } from '@testing-library/react' +import { Provider } from 'react-redux' + +export function jestRender(component: React.ReactElement) { + return render({component}) +} + + @@ -0,0 +1,28 @@ +//@ts-nocheck +export function windowMock() { + let originalLocation = window.location // Сохраняем оригинальный window.location + + beforeEach(() => { + // Удаляем оригинальный window.location + delete window.location + + // Мокируем window.location + window.location = { + href: '', + assign: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + protocol: 'http:', + host: 'localhost', + pathname: '/', + search: '', + hash: '', + ...originalLocation, // Сохраняем оригинальные методы + } + }) + afterEach(() => { + // Возвращаем оригинальный window.location + // @ts-ignore + window.location = originalLocation + }) +} @@ -1,19 +1,8 @@ -import axios from 'axios' +import { api } from '#/shared/api' -import { API_URL } from '#/shared/lib/constants' - -export const changePassword = async (email: string, password: string, newPassword: string, token?: string) => { - return await axios.patch( - API_URL + `/auth/business-host/account/change-pass/${email}`, - { - password_1: password, - password_2: newPassword, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - validateStatus: (status) => status < 500 - } - ) +export const changePassword = async (email: string, password_1: string, password_2: string) => { + return await api.patch<{ detail: string }>(`/auth/business-host/account/change-pass/${email}`, { + password_1, + password_2, + }) } @@ -4,6 +4,7 @@ import { useState } from 'react' import { getModalById, PLATE_CHANGE_PASSWORD } from '#/features/modals' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { useForm } from 'react-hook-form' +import { makePrivateRequest } from '#/shared/api' interface ChangePasswordForm { password: string @@ -23,34 +24,27 @@ export const useChangePassword = () => { setValue, } = useForm() const { showMessage } = useShowDataStore() - const { data: session } = useSession() const [passwordShowed, setPasswordShowed] = useState(false) - const onSubmit = handleSubmit(async (data, event) => { - if (!session) return + const onSubmit = handleSubmit( + makePrivateRequest(async ({ password, newPassword }) => { + if (password !== newPassword) { + showMessage('Пароли не совпадают') + setError('password', { message: 'Пароли не совпадают' }) + return setError('newPassword', { message: 'Пароли не совпадают' }) + } - if (data.password !== data.newPassword) { - showMessage('Пароли не совпадают!', 'error') - setError('password', { message: 'Пароли не совпадают' }) - return setError('newPassword', { message: 'Пароли не совпадают' }) - } + const { data, status } = await changePassword(modal.getStoreProperty('email')!, password, newPassword) - const { data: res, status } = await changePassword( - modal.getStoreProperty('email')!, - data.password, - data.newPassword, - session.access - ) + if (status !== 200) return showMessage(data.detail) - if (status == 200) { showMessage('Пароль изменен успешно!', 'success') + modal.setState(false) - return - } - showMessage((res as { detail: string }).detail) - reset() - }) + reset() + }) + ) return { onSubmit, register, watch, reset, errors, trigger, setValue, passwordShowed, setPasswordShowed } } @@ -0,0 +1,100 @@ +import '@testing-library/jest-dom' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' +import { jestRender } from '#/../jest/utils/render' +import { windowMock } from '#/../jest/utils/window-mock' +import { ChangePasswordPlate } from './change-password' +import { PLATE_CHANGE_PASSWORD } from '#/features/modals' +import { getModalTest } from '#/../jest/utils/modal-tester' +import { api } from '#/shared/api/instance' + +jest.mock('next/router') +jest.mock('@sentry/nextjs') + +jest.mock('#/shared/api/instance') + +const showMessageMock = jest.fn() +const useSessionMock = jest.fn() + +jest.mock('next-auth/react', () => { + return { + useSession: () => useSessionMock(), + } +}) + +jest.mock('#/shared/lib/hooks/use-show-data', () => { + return { + useShowDataStore: jest.fn(() => { + return { + showMessage: showMessageMock, + } + }), + } +}) + +useSessionMock.mockReturnValue({ + data: { + access: 'something access', + }, +}) + +windowMock() + +describe('chage-password', () => { + it('successfully change password', async () => { + jestRender() + + const modal = await getModalTest(PLATE_CHANGE_PASSWORD) + + act(() => { + modal.setState(true, { + email: 'aleksander.freelancer@gmail.com', + }) + }) + ;(api.patch as jest.Mock).mockResolvedValue({ + status: 200, + data: { detail: 'Host password has been updated' }, + }) + + const password = screen.getByPlaceholderText('Пароль') + const newPassword = screen.getByPlaceholderText('Повторить пароль') + const button = screen.getByText('Сменить пароль') + + fireEvent.change(password, { target: { value: 'h7O5vF$M' } }) + fireEvent.change(newPassword, { target: { value: 'h7O5vF$M' } }) + + act(() => { + fireEvent.click(button) + }) + + await waitFor(() => { + expect(showMessageMock).toHaveBeenCalledWith('Пароль изменен успешно!', 'success') + }) + }) + + it('not equal passwords', async () => { + jestRender() + + const modal = await getModalTest(PLATE_CHANGE_PASSWORD) + + act(() => { + modal.setState(true, { + email: 'aleksander.freelancer@gmail.com', + }) + }) + + const password = screen.getByPlaceholderText('Пароль') + const newPassword = screen.getByPlaceholderText('Повторить пароль') + const button = screen.getByText('Сменить пароль') + + fireEvent.change(password, { target: { value: 'h7O5vF$M' } }) + fireEvent.change(newPassword, { target: { value: 'h7O5vF$Md' } }) + + act(() => { + fireEvent.click(button) + }) + + await waitFor(() => { + expect(showMessageMock).toHaveBeenCalledWith('Пароли не совпадают') + }) + }) +}) @@ -1,6 +1,5 @@ import { getModalById, PLATE_CHANGE_PASSWORD, PlateTemplate } from '#/features/modals' import styles from './change-password.module.scss' -import React, { useEffect } from 'react' import { CommonButton } from '#/shared/ui/button' import { CommonInput } from '#/shared/ui/common-input' import { useChangePassword } from '../lib/use-change-password' @@ -9,30 +8,19 @@ import ClosedEyeSvg from '#/assets/svg/closed-eye.svg?react' export const ChangePasswordPlate = () => { const modal = getModalById(PLATE_CHANGE_PASSWORD) - const { onSubmit, register, errors, reset, trigger, watch, setValue, passwordShowed, setPasswordShowed } = useChangePassword() + + const { onSubmit, register, errors, reset, trigger, passwordShowed, setPasswordShowed } = useChangePassword() return ( - { - reset() - }} - > -
+ reset()}> + trigger()} onSubmit={onSubmit}>

Смена пароля

{ - setValue('password', e.target.value) - trigger('password') - }} label='Новый пароль' variant='outline' placeholder='Пароль' @@ -48,18 +36,12 @@ export const ChangePasswordPlate = () => { )} } - // onInput={() => trigger('password')} /> { - setValue('newPassword', e.target.value) - trigger('newPassword') - }} label='Повторить пароль' variant='outline' placeholder='Повторить пароль' @@ -74,21 +56,13 @@ export const ChangePasswordPlate = () => { } error={errors.newPassword} - // onInput={() => trigger('password')} />
Сменить пароль - { - modal.setState(false) - reset() - }} - type='button' - > + modal.setState(false)} type='button'> Отмена
@@ -1,4 +1,3 @@ -'use client' import { useEffect } from 'react' import { c } from '#/shared' import { usePlatesStore } from '../model' @@ -44,8 +43,6 @@ export default function PlatesTemplate({ const modal = getModal(id) - const { theme } = useThemeAndDevice() - useEffect(() => setModal(id), []) return ( @@ -1,4 +1,3 @@ -// @ts-nocheck import React, { useState } from 'react' import { useForm } from 'react-hook-form' import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' @@ -1,11 +1,12 @@ .container { padding-bottom: 0; + background-color: #373737; } .modal { min-width: 450px; width: 100%; - padding-top: 35px; + padding-top: 35px; &__buttons { display: flex; @@ -3,15 +3,20 @@ import { useThemeAndDevice } from '#/shared/lib/hooks' import { Button } from '@mui/material' import { signIn } from 'next-auth/react' import Image from 'next/image' -import React from 'react' +import React, { useState } from 'react' interface YandexAuthButtonProps {} export const YandexAuthButton = ({}: YandexAuthButtonProps) => { const { theme } = useThemeAndDevice() + const [hovered, setHovered] = useState(false) + return (