Files
iistwin/tests/bookmarks.test.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

332 lines
9.7 KiB
TypeScript

import { vi, describe, it, expect, beforeEach } from 'vitest';
const mockStorage = vi.hoisted(() => ({
getUserWithOrganization: vi.fn(),
createBookmark: vi.fn(),
createBookmarkFolder: vi.fn(),
getBookmarkFolder: vi.fn(),
getBookmarksByOrganization: vi.fn(),
getBookmarksByFolder: vi.fn(),
getUser: vi.fn(),
getFormStatuses: vi.fn(),
}));
vi.mock('../server/db', () => ({
db: {},
pool: { query: vi.fn().mockResolvedValue({ rows: [] }) },
withTenant: (_orgId: number, fn: () => unknown) => fn(),
openTenantCtx: vi.fn().mockResolvedValue({
run: (fn: () => void) => fn(),
release: vi.fn(),
}),
openSuperAdminCtx: vi.fn().mockResolvedValue(undefined),
_tenantCtx: { getStore: vi.fn().mockReturnValue(null) },
}));
vi.mock('../server/storage', () => ({ storage: mockStorage }));
vi.mock('../server/services/notification.service', () => ({
notificationService: {
emit: vi.fn(),
on: vi.fn(),
sendNotification: vi.fn().mockResolvedValue(undefined),
processEvent: vi.fn().mockResolvedValue(undefined),
},
EVENT_TYPES: {},
}));
vi.mock('../server/services/webhook.service', () => ({
webhookService: {
dispatchEvent: vi.fn().mockResolvedValue(undefined),
processWebhook: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock('../server/utils/webhook', () => ({
sendWebhook: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('../server/utils/s3', () => ({
isS3Enabled: false,
ensureS3Bucket: vi.fn().mockResolvedValue(undefined),
streamFromS3: vi.fn().mockResolvedValue(null),
deleteFromS3: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('../server/utils/audit', () => ({
logAudit: vi.fn().mockResolvedValue(undefined),
getClientIp: vi.fn().mockReturnValue('127.0.0.1'),
}));
import express from 'express';
import request from 'supertest';
import jwt from 'jsonwebtoken';
import chatRouter from '../server/routes/chat.routes';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET as string;
function makeValidToken(userId: number, organizationId: number): string {
return jwt.sign(
{ userId, organizationId, role: 'user' },
ACCESS_SECRET,
{ issuer: 'workflow-system', audience: 'workflow-users', expiresIn: '1h' },
);
}
function buildApp() {
const app = express();
app.use(express.json());
app.use(chatRouter);
return app;
}
const TEST_USER = {
id: 42,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
role: 'user',
isActive: true,
organizationId: 7,
organization: { id: 7, isActive: true, billingBlocked: false },
};
describe('POST /api/bookmarks — auth guard', () => {
const app = buildApp();
const validBookmarkBody = {
name: 'My Bookmark',
type: 'dashboard',
};
it('returns 401 when Authorization header is absent', async () => {
const res = await request(app)
.post('/api/bookmarks')
.send(validBookmarkBody);
expect(res.status).toBe(401);
expect(res.body).toMatchObject({ error: expect.any(String) });
});
it('returns 403 when token is invalid / tampered', async () => {
const res = await request(app)
.post('/api/bookmarks')
.set('Authorization', 'Bearer not-a-real-token')
.send(validBookmarkBody);
expect(res.status).toBe(403);
});
it('returns 403 when token is signed with the wrong secret', async () => {
const badToken = jwt.sign(
{ userId: 1, organizationId: 1, role: 'user' },
'completely-wrong-secret',
);
const res = await request(app)
.post('/api/bookmarks')
.set('Authorization', `Bearer ${badToken}`)
.send(validBookmarkBody);
expect(res.status).toBe(403);
});
it('returns 401 and does not write to storage when token is valid but user does not exist', async () => {
mockStorage.getUserWithOrganization.mockResolvedValue(undefined);
mockStorage.createBookmark.mockClear();
const token = makeValidToken(99, 1);
const res = await request(app)
.post('/api/bookmarks')
.set('Authorization', `Bearer ${token}`)
.send(validBookmarkBody);
expect(res.status).toBe(401);
expect(mockStorage.createBookmark).not.toHaveBeenCalled();
});
it('returns 401 and does not write to storage when user account is inactive', async () => {
mockStorage.getUserWithOrganization.mockResolvedValue({
...TEST_USER,
isActive: false,
});
mockStorage.createBookmark.mockClear();
const token = makeValidToken(TEST_USER.id, TEST_USER.organizationId);
const res = await request(app)
.post('/api/bookmarks')
.set('Authorization', `Bearer ${token}`)
.send(validBookmarkBody);
expect(res.status).toBe(401);
expect(mockStorage.createBookmark).not.toHaveBeenCalled();
});
describe('with a valid token', () => {
beforeEach(() => {
mockStorage.getUserWithOrganization.mockResolvedValue(TEST_USER);
mockStorage.createBookmark.mockResolvedValue({
id: 1,
...validBookmarkBody,
userId: TEST_USER.id,
organizationId: TEST_USER.organizationId,
createdBy: TEST_USER.id,
position: 0,
folderId: null,
targetId: null,
formId: null,
url: null,
icon: null,
viewConfig: null,
createdAt: new Date(),
updatedAt: new Date(),
});
});
it('returns 201 and the created bookmark contains the authenticated user\'s id', async () => {
const token = makeValidToken(TEST_USER.id, TEST_USER.organizationId);
const res = await request(app)
.post('/api/bookmarks')
.set('Authorization', `Bearer ${token}`)
.send(validBookmarkBody);
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.bookmark.userId).toBe(TEST_USER.id);
});
it('passes the authenticated user\'s id to storage.createBookmark', async () => {
const token = makeValidToken(TEST_USER.id, TEST_USER.organizationId);
await request(app)
.post('/api/bookmarks')
.set('Authorization', `Bearer ${token}`)
.send(validBookmarkBody);
expect(mockStorage.createBookmark).toHaveBeenCalledWith(
expect.objectContaining({ userId: TEST_USER.id }),
);
});
});
});
describe('POST /api/bookmark-folders — auth guard', () => {
const app = buildApp();
const validFolderBody = {
name: 'My Folder',
};
it('returns 401 when Authorization header is absent', async () => {
const res = await request(app)
.post('/api/bookmark-folders')
.send(validFolderBody);
expect(res.status).toBe(401);
expect(res.body).toMatchObject({ error: expect.any(String) });
});
it('returns 403 when token is invalid / tampered', async () => {
const res = await request(app)
.post('/api/bookmark-folders')
.set('Authorization', 'Bearer garbage-token')
.send(validFolderBody);
expect(res.status).toBe(403);
});
it('returns 403 when token is signed with the wrong secret', async () => {
const badToken = jwt.sign(
{ userId: 1, organizationId: 1, role: 'user' },
'wrong-secret',
);
const res = await request(app)
.post('/api/bookmark-folders')
.set('Authorization', `Bearer ${badToken}`)
.send(validFolderBody);
expect(res.status).toBe(403);
});
it('returns 401 and does not write to storage when token is valid but user does not exist', async () => {
mockStorage.getUserWithOrganization.mockResolvedValue(undefined);
mockStorage.createBookmarkFolder.mockClear();
const token = makeValidToken(99, 1);
const res = await request(app)
.post('/api/bookmark-folders')
.set('Authorization', `Bearer ${token}`)
.send(validFolderBody);
expect(res.status).toBe(401);
expect(mockStorage.createBookmarkFolder).not.toHaveBeenCalled();
});
it('returns 401 and does not write to storage when user account is inactive', async () => {
mockStorage.getUserWithOrganization.mockResolvedValue({
...TEST_USER,
isActive: false,
});
mockStorage.createBookmarkFolder.mockClear();
const token = makeValidToken(TEST_USER.id, TEST_USER.organizationId);
const res = await request(app)
.post('/api/bookmark-folders')
.set('Authorization', `Bearer ${token}`)
.send(validFolderBody);
expect(res.status).toBe(401);
expect(mockStorage.createBookmarkFolder).not.toHaveBeenCalled();
});
describe('with a valid token', () => {
beforeEach(() => {
mockStorage.getUserWithOrganization.mockResolvedValue(TEST_USER);
mockStorage.createBookmarkFolder.mockResolvedValue({
id: 10,
...validFolderBody,
userId: TEST_USER.id,
organizationId: TEST_USER.organizationId,
createdBy: TEST_USER.id,
parentId: null,
position: 0,
isExpanded: true,
createdAt: new Date(),
updatedAt: new Date(),
});
});
it('returns 201 and the created folder contains the authenticated user\'s id', async () => {
const token = makeValidToken(TEST_USER.id, TEST_USER.organizationId);
const res = await request(app)
.post('/api/bookmark-folders')
.set('Authorization', `Bearer ${token}`)
.send(validFolderBody);
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.folder.userId).toBe(TEST_USER.id);
});
it('passes the authenticated user\'s id to storage.createBookmarkFolder', async () => {
const token = makeValidToken(TEST_USER.id, TEST_USER.organizationId);
await request(app)
.post('/api/bookmark-folders')
.set('Authorization', `Bearer ${token}`)
.send(validFolderBody);
expect(mockStorage.createBookmarkFolder).toHaveBeenCalledWith(
expect.objectContaining({ userId: TEST_USER.id }),
);
});
});
});