diff --git a/apps/web/vitest.config.mts b/apps/web/vitest.config.mts index 4915b13..cf8d8cb 100644 --- a/apps/web/vitest.config.mts +++ b/apps/web/vitest.config.mts @@ -6,7 +6,7 @@ export default defineConfig({ plugins: [tsconfigPaths(), react()], test: { environment: 'jsdom', - exclude: ['**/e2e/**', '**/*.spec.ts'], + exclude: ['**/e2e/**', '**/*.spec.ts', '**/node_modules/**'], include: ['**/*.test.{ts,tsx}'], }, }); diff --git a/packages/graphql/api/slots.test.js b/packages/graphql/api/slots.test.js new file mode 100644 index 0000000..3fde691 --- /dev/null +++ b/packages/graphql/api/slots.test.js @@ -0,0 +1,303 @@ +import { getClientWithToken } from '../apollo/client'; +import * as GQL from '../types'; +import { ERRORS, SlotsService } from './slots'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../apollo/client'); +vi.mock('./services'); +vi.mock('../config/env', () => { + return { + env: { + BOT_TOKEN: 'test', + LOGIN_GRAPHQL: 'test', + PASSWORD_GRAPHQL: 'test', + URL_GRAPHQL: 'test', + }, + }; +}); + +const mockGetClientWithToken = vi.mocked(getClientWithToken); + +describe('SlotsService', () => { + let slotsService; + const mockUser = { telegramId: 123_456_789 }; + + const mockCustomer = { + documentId: 'customer-123', + firstName: 'John', + lastName: 'Doe', + telegramId: 123_456_789, + }; + + const mockSlot = { + datetime_end: '2024-01-01T11:00:00Z', + datetime_start: '2024-01-01T10:00:00Z', + documentId: 'slot-123', + master: mockCustomer, + orders: [], + state: GQL.Enum_Slot_State.Open, + }; + + const mockGetCustomerResult = { + data: { + customers: [mockCustomer], + }, + }; + + const mockGetSlotResult = { + data: { + slot: mockSlot, + }, + }; + + beforeEach(() => { + slotsService = new SlotsService(mockUser); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('updateSlot', () => { + const mockVariables = { + data: { + datetime_end: '2024-01-01T11:00:00Z', + datetime_start: '2024-01-01T10:00:00Z', + state: GQL.Enum_Slot_State.Open, + }, + documentId: 'slot-123', + }; + + const mockMutationResult = { + data: { + updateSlot: mockSlot, + }, + errors: undefined, + }; + + it('should successfully update slot when user has permission', async () => { + const mockMutate = vi.fn().mockResolvedValue(mockMutationResult); + const mockQuery = vi + .fn() + .mockResolvedValueOnce(mockGetCustomerResult) + .mockResolvedValueOnce(mockGetSlotResult); + + mockGetClientWithToken.mockResolvedValue({ + mutate: mockMutate, + query: mockQuery, + }); + + const result = slotsService.updateSlot(mockVariables); + + await expect(result).resolves.toBe(mockMutationResult.data); + }); + + it('should throw error when user does not have permission', async () => { + const unrelatedCustomer = { + ...mockCustomer, + documentId: 'different-customer-123', + }; + + const mockQuery = vi + .fn() + .mockResolvedValueOnce({ + data: { customers: [unrelatedCustomer] }, + }) + .mockResolvedValueOnce({ + data: { slot: mockSlot }, // slot принадлежит другому пользователю + }); + + mockGetClientWithToken.mockResolvedValue({ + mutate: vi.fn(), + query: mockQuery, + }); + + const result = slotsService.updateSlot(mockVariables); + + await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION); + }); + + it('should throw error when slot does not exist', async () => { + const mockQuery = vi + .fn() + .mockResolvedValueOnce(mockGetCustomerResult) + .mockResolvedValueOnce({ + data: { slot: null }, // slot не найден + }); + + mockGetClientWithToken.mockResolvedValue({ + mutate: vi.fn(), + query: mockQuery, + }); + + const result = slotsService.updateSlot(mockVariables); + + await expect(result).rejects.toThrow(); + }); + + it('should throw error when customer is not found', async () => { + const mockQuery = vi.fn().mockResolvedValue({ + data: { customers: [] }, // пользователь не найден + }); + + mockGetClientWithToken.mockResolvedValue({ + mutate: vi.fn(), + query: mockQuery, + }); + + const result = slotsService.updateSlot(mockVariables); + + await expect(result).rejects.toThrow('Customer not found'); + }); + }); + + describe('checkPermission', () => { + const mockVariables = { + documentId: 'slot-123', + }; + + it('should not throw error when user has permission', async () => { + const mockQuery = vi + .fn() + .mockResolvedValueOnce(mockGetCustomerResult) + .mockResolvedValueOnce(mockGetSlotResult); + + mockGetClientWithToken.mockResolvedValue({ + query: mockQuery, + }); + + const result = slotsService.checkPermission(mockVariables); + + await expect(result).resolves.toBeUndefined(); + }); + + it('should throw error when user does not have permission', async () => { + const unrelatedCustomer = { + ...mockCustomer, + documentId: 'different-customer-123', + }; + + const mockQuery = vi + .fn() + .mockResolvedValueOnce({ + data: { customers: [unrelatedCustomer] }, + }) + .mockResolvedValueOnce({ + data: { slot: mockSlot }, // slot принадлежит другому пользователю + }); + + mockGetClientWithToken.mockResolvedValue({ + query: mockQuery, + }); + + const result = slotsService.checkPermission(mockVariables); + + await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION); + }); + + it('should throw error when slot does not exist', async () => { + const mockQuery = vi + .fn() + .mockResolvedValueOnce(mockGetCustomerResult) + .mockResolvedValueOnce({ + data: { slot: null }, // slot не найден + }); + + mockGetClientWithToken.mockResolvedValue({ + query: mockQuery, + }); + + const result = slotsService.checkPermission(mockVariables); + + await expect(result).rejects.toThrow(); + }); + }); + + describe('deleteSlot', () => { + const mockVariables = { + documentId: 'slot-123', + }; + + const mockMutationResult = { + data: { + deleteSlot: { + documentId: 'slot-123', + }, + }, + errors: undefined, + }; + + it('should successfully delete slot when no orders', async () => { + const mockMutate = vi.fn().mockResolvedValue(mockMutationResult); + const mockQuery = vi + .fn() + .mockResolvedValueOnce(mockGetCustomerResult) + .mockResolvedValue(mockGetSlotResult); + + mockGetClientWithToken.mockResolvedValue({ + mutate: mockMutate, + query: mockQuery, + }); + + const result = slotsService.deleteSlot(mockVariables); + + await expect(result).resolves.toBe(mockMutationResult.data); + }); + + it('should throw error when slot has orders', async () => { + const slotWithOrders = { + ...mockSlot, + orders: [ + { + datetime_end: '2024-01-01T11:00:00Z', + datetime_start: '2024-01-01T10:00:00Z', + documentId: 'order-123', + state: GQL.Enum_Order_State.Scheduled, + }, + ], + }; + + const mockQuery = vi + .fn() + .mockResolvedValueOnce(mockGetCustomerResult) + .mockResolvedValue({ + data: { slot: slotWithOrders }, // slot с заказами + }); + + mockGetClientWithToken.mockResolvedValue({ + mutate: vi.fn(), + query: mockQuery, + }); + + const result = slotsService.deleteSlot(mockVariables); + + await expect(result).rejects.toThrow(ERRORS.HAS_ORDERS); + }); + + it('should throw error when user does not have permission', async () => { + const unrelatedCustomer = { + ...mockCustomer, + documentId: 'different-customer-123', + }; + + const mockQuery = vi + .fn() + .mockResolvedValueOnce({ + data: { customers: [unrelatedCustomer] }, + }) + .mockResolvedValueOnce({ + data: { slot: mockSlot }, // slot принадлежит другому пользователю + }); + + mockGetClientWithToken.mockResolvedValue({ + mutate: vi.fn(), + query: mockQuery, + }); + + const result = slotsService.deleteSlot(mockVariables); + + await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION); + }); + }); +}); diff --git a/packages/graphql/api/slots.ts b/packages/graphql/api/slots.ts index 405b4c0..b762488 100644 --- a/packages/graphql/api/slots.ts +++ b/packages/graphql/api/slots.ts @@ -6,7 +6,7 @@ import { type VariablesOf } from '@graphql-typed-document-node/core'; import { getMinutes } from '@repo/utils/datetime-format'; import dayjs from 'dayjs'; -const ERRORS = { +export const ERRORS = { HAS_ORDERS: 'Slot has orders', MISSING_DATE: 'Missing date', MISSING_SERVICE: 'Missing service', diff --git a/packages/graphql/vitest.config.mts b/packages/graphql/vitest.config.mts new file mode 100644 index 0000000..3f22771 --- /dev/null +++ b/packages/graphql/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + exclude: ['**/e2e/**', '**/*.spec.ts', '**/node_modules/**'], + include: ['**/*.test.{js,ts}'], + }, +});