diff --git a/__tests__/api.test.js b/__tests__/api.test.js index b81c2ad..d7c8bc0 100644 --- a/__tests__/api.test.js +++ b/__tests__/api.test.js @@ -1,32 +1,92 @@ const request = require('supertest'); -const { app, pool } = require('../app'); + +// Мокаем pg +jest.mock('pg', () => { + const mPool = { + connect: jest.fn(), + query: jest.fn(), + end: jest.fn(), + }; + return { Pool: jest.fn(() => mPool) }; +}); + +// Мокаем middleware ensureAuthenticated +jest.mock('../app', () => { + const original = jest.requireActual('../app'); + // Переопределяем ensureAuthenticated + original.ensureAuthenticated = (req, res, next) => { + req.user = { id: 1, name: 'Test User' }; + next(); + }; + return original; +}); + +const { app, pool, ensureAuthenticated } = require('../app'); + +// Мокаем middleware аутентификации +jest.mock('passport', () => ({ + initialize: () => (req, res, next) => next(), + session: () => (req, res, next) => next(), + use: jest.fn(), + serializeUser: jest.fn(), + deserializeUser: jest.fn(), + authenticate: () => (req, res, next) => next() +})); + +// Поскольку app.js уже инициализировал роуты со старым ensureAuthenticated, +// мока ensureAuthenticated недостаточно для уже созданных роутов. +// Мы должны подменить isAuthenticated и user прямо в middleware, который +// выполнится *до* роутов. Но так как роуты уже добавлены, мы можем встроить +// middleware в самое начало стека Express, либо использовать jest.spyOn +// на passport или express-session, но проще подменить req в app.request. +const express = require('express'); +app.request.isAuthenticated = function() { return true; }; +Object.defineProperty(app.request, 'user', { + get: function() { return { id: 1, name: 'Test User' }; }, + configurable: true +}); let server; -// Перед выполнением всех тестов, запускаем сервер beforeAll(done => { server = app.listen(done); }); -// После выполнения всех тестов, закрываем сервер и пул соединений afterAll(done => { server.close(() => { - pool.end(done); + done(); }); }); describe('API Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('POST /api/houses должен создавать новый дом', async () => { + pool.query.mockResolvedValueOnce({ rows: [{ id: 1 }] }); + const response = await request(app) .post('/api/houses') .send({ name: 'Тестовый дом' }); + expect(response.statusCode).toBe(201); expect(response.body.name).toBe('Тестовый дом'); + expect(response.body.id).toBe(1); + expect(pool.query).toHaveBeenCalledWith( + 'INSERT INTO houses (user_id, name) VALUES ($1, $2) RETURNING id', + [1, 'Тестовый дом'] + ); }); test('GET /api/houses должен возвращать список домов', async () => { + pool.query.mockResolvedValueOnce({ rows: [{ id: 1, name: 'Дом 1' }, { id: 2, name: 'Дом 2' }] }); + const response = await request(app).get('/api/houses'); + expect(response.statusCode).toBe(200); expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(2); + expect(pool.query).toHaveBeenCalledWith('SELECT * FROM houses WHERE user_id = $1', [1]); }); }); \ No newline at end of file diff --git a/__tests__/upload-excel.test.js b/__tests__/upload-excel.test.js new file mode 100644 index 0000000..666ebcf --- /dev/null +++ b/__tests__/upload-excel.test.js @@ -0,0 +1,143 @@ +const request = require('supertest'); + +jest.mock('pg', () => { + const mClient = { + query: jest.fn(), + release: jest.fn(), + }; + const mPool = { + connect: jest.fn(() => mClient), + query: jest.fn(), + end: jest.fn(), + }; + return { Pool: jest.fn(() => mPool) }; +}); + +const { app, pool } = require('../app'); +const ExcelJS = require('exceljs'); + +// Mock passport and session middleware just like api.test.js +jest.mock('passport', () => ({ + initialize: () => (req, res, next) => next(), + session: () => (req, res, next) => next(), + use: jest.fn(), + serializeUser: jest.fn(), + deserializeUser: jest.fn(), + authenticate: () => (req, res, next) => next() +})); + +app.request.isAuthenticated = function() { return true; }; +Object.defineProperty(app.request, 'user', { + get: function() { return { id: 1, name: 'Test User' }; }, + configurable: true +}); + +let server; + +beforeAll(done => { + server = app.listen(done); +}); + +afterAll(done => { + server.close(() => { + done(); + }); +}); + +describe('POST /api/upload-excel', () => { + let mockClient; + + beforeEach(() => { + jest.clearAllMocks(); + // Setup mockClient + mockClient = { + query: jest.fn(), + release: jest.fn(), + }; + pool.connect.mockResolvedValue(mockClient); + }); + + test('should return 400 if no file is uploaded', async () => { + const response = await request(app).post('/api/upload-excel'); + expect(response.statusCode).toBe(400); + expect(response.body.error).toBe('Файл не загружен.'); + }); + + test('should successfully import Excel file data (Happy Path)', async () => { + // Create an in-memory Excel file + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet 1'); + + // Add header row (mock the rows expected by parsing) + worksheet.addRow(['Name', 'Type', 'Description', 'House', 'Room', 'ParentItem']); + // Add data row + worksheet.addRow(['Item1', 'Gadget', 'A cool gadget', 'My House', 'Living Room', '']); + + const buffer = await workbook.xlsx.writeBuffer(); + + // Setup mock logic for DB queries + // findId for houses, rooms, items respectively + pool.query + .mockResolvedValueOnce({ rows: [] }) // House not found -> will trigger INSERT + .mockResolvedValueOnce({ rows: [] }) // Room not found -> will trigger INSERT + // item without parent doesn't trigger a findId + + // Mock client queries within transaction + mockClient.query + .mockResolvedValueOnce() // BEGIN + .mockResolvedValueOnce({ rows: [{ id: 100 }] }) // INSERT house + .mockResolvedValueOnce({ rows: [{ id: 200 }] }) // INSERT room + .mockResolvedValueOnce() // INSERT item + .mockResolvedValueOnce(); // COMMIT + + const response = await request(app) + .post('/api/upload-excel') + .attach('excelFile', buffer, 'test.xlsx'); + + expect(response.statusCode).toBe(200); + expect(response.body.message).toBe('Данные успешно импортированы.'); + + // Validate transaction operations + expect(mockClient.query).toHaveBeenNthCalledWith(1, 'BEGIN'); + expect(mockClient.query).toHaveBeenCalledWith( + 'INSERT INTO houses(name, user_id) VALUES($1, $2) RETURNING id', + ['My House', 1] + ); + expect(mockClient.query).toHaveBeenCalledWith( + 'INSERT INTO rooms(name, house_id, user_id) VALUES($1, $2, $3) RETURNING id', + ['Living Room', 100, 1] + ); + expect(mockClient.query).toHaveBeenCalledWith( + 'INSERT INTO items(name, type, description, room_id, parent_item_id, user_id) VALUES($1, $2, $3, $4, $5, $6)', + ['Item1', 'Gadget', 'A cool gadget', 200, null, 1] + ); + expect(mockClient.query).toHaveBeenLastCalledWith('COMMIT'); + expect(mockClient.release).toHaveBeenCalled(); + }); + + test('should rollback transaction and return 500 on database error', async () => { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet 1'); + worksheet.addRow(['Name', 'Type', 'Description', 'House', 'Room', 'ParentItem']); + worksheet.addRow(['ItemError', 'Gadget', 'Will cause error', 'My House', 'Living Room', '']); + + const buffer = await workbook.xlsx.writeBuffer(); + + // Simulate database failure during INSERT + mockClient.query + .mockResolvedValueOnce() // BEGIN + .mockRejectedValueOnce(new Error('Simulated DB Error')); + + const response = await request(app) + .post('/api/upload-excel') + .attach('excelFile', buffer, 'test.xlsx'); + + expect(response.statusCode).toBe(500); + expect(response.body.error).toBe('Ошибка при импорте данных.'); + + // Verify ROLLBACK was called + expect(mockClient.query).toHaveBeenCalledWith('BEGIN'); + expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK'); + expect(mockClient.release).toHaveBeenCalled(); + }); +});