Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions __tests__/api.test.js
Original file line number Diff line number Diff line change
@@ -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]);
});
});
143 changes: 143 additions & 0 deletions __tests__/upload-excel.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});