- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
332 lines
9.7 KiB
TypeScript
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 }),
|
|
);
|
|
});
|
|
});
|
|
});
|