Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit introduces several utility functions in the `utils.js` file, including `getSchemaTypeRef`, `forEachSchemaPath`, and `collectIdsAtPath`, to streamline schema reference handling. Additionally, it refactors the `modelHasRef` and `getFieldsByRef` functions to utilize these new utilities, improving code readability and maintainability. The email message schema is updated to support multiple recipients with enhanced attachment handling, ensuring better management of email content and references. Tests are added to validate the new functionality, reinforcing the integrity of email message processing.
219 lines
6.6 KiB
JavaScript
219 lines
6.6 KiB
JavaScript
import { jest } from '@jest/globals';
|
|
|
|
// Mock src/database/utils.js (where generateId and convertObjectIdStringsInFilter live)
|
|
jest.unstable_mockModule('../utils.js', () => ({
|
|
generateId: jest.fn(() => () => 'test-id'),
|
|
convertObjectIdStringsInFilter: jest.fn((filter) => filter),
|
|
}));
|
|
|
|
// Mock src/utils.js (where most database.js helpers live)
|
|
jest.unstable_mockModule('../../utils.js', () => ({
|
|
deleteAuditLog: jest.fn(),
|
|
deleteNotification: jest.fn(),
|
|
distributeDelete: jest.fn(),
|
|
editAuditLog: jest.fn(),
|
|
editNotification: jest.fn(),
|
|
expandObjectIds: jest.fn((obj) => obj),
|
|
flatternObjectIds: jest.fn((object) => {
|
|
if (!object || typeof object !== 'object') {
|
|
return object;
|
|
}
|
|
|
|
const result = {};
|
|
|
|
for (const [key, value] of Object.entries(object)) {
|
|
if (value && typeof value === 'object' && value._id) {
|
|
result[key] = value._id;
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}),
|
|
getFieldsByRef: jest.fn(() => []),
|
|
collectIdsAtPath: jest.fn(() => []),
|
|
getQueryToCacheKey: jest.fn(({ model, id }) => `${model}:${id}`),
|
|
modelHasRef: jest.fn(() => false),
|
|
newAuditLog: jest.fn(),
|
|
distributeNew: jest.fn(),
|
|
distributeUpdate: jest.fn(),
|
|
distributeChildUpdate: jest.fn(),
|
|
distributeChildDelete: jest.fn(),
|
|
distributeChildNew: jest.fn(),
|
|
distributeStats: jest.fn(),
|
|
}));
|
|
|
|
jest.unstable_mockModule('../redis.js', () => ({
|
|
redisServer: {
|
|
getKey: jest.fn(),
|
|
setKey: jest.fn(),
|
|
deleteKey: jest.fn(),
|
|
getKeysByPattern: jest.fn(() => []), // Return empty array to avoid iterable error
|
|
},
|
|
}));
|
|
|
|
jest.unstable_mockModule('../../services/misc/model.js', () => ({
|
|
getAllModels: jest.fn(() => []),
|
|
getModelByName: jest.fn(() => null),
|
|
}));
|
|
|
|
jest.unstable_mockModule('../schemas/management/auditlog.schema.js', () => ({
|
|
auditLogModel: {
|
|
find: jest.fn(),
|
|
create: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock fileModel specifically as it's imported by database.js
|
|
jest.unstable_mockModule('../schemas/management/file.schema.js', () => ({
|
|
fileModel: {
|
|
findById: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
// Now import the database utilities
|
|
const { listObjects, getObject, newObject, editObject, deleteObject } =
|
|
await import('../database.js');
|
|
const { editAuditLog, editNotification } = await import('../../utils.js');
|
|
|
|
describe('Database Utilities (CRUD)', () => {
|
|
let mockModel;
|
|
|
|
beforeEach(() => {
|
|
mockModel = {
|
|
modelName: 'TestModel',
|
|
find: jest.fn().mockReturnThis(),
|
|
findById: jest.fn().mockReturnThis(),
|
|
findByIdAndUpdate: jest.fn().mockReturnThis(),
|
|
findByIdAndDelete: jest.fn().mockReturnThis(),
|
|
create: jest.fn(),
|
|
sort: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
limit: jest.fn().mockReturnThis(),
|
|
populate: jest.fn().mockReturnThis(),
|
|
select: jest.fn().mockReturnThis(),
|
|
lean: jest.fn().mockReturnThis(),
|
|
exec: jest.fn(),
|
|
};
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('listObjects', () => {
|
|
it('should return a list of objects', async () => {
|
|
const mockData = [{ _id: '1', name: 'Test' }];
|
|
mockModel.lean.mockResolvedValue(mockData);
|
|
|
|
const result = await listObjects({ model: mockModel });
|
|
|
|
expect(mockModel.find).toHaveBeenCalled();
|
|
expect(result).toEqual(mockData);
|
|
});
|
|
|
|
it('should handle pagination', async () => {
|
|
await listObjects({ model: mockModel, page: 2, limit: 10 });
|
|
expect(mockModel.skip).toHaveBeenCalledWith(10);
|
|
expect(mockModel.limit).toHaveBeenCalledWith(10);
|
|
});
|
|
});
|
|
|
|
describe('getObject', () => {
|
|
it('should return a single object by ID', async () => {
|
|
const mockData = { _id: '123', name: 'Test' };
|
|
mockModel.lean.mockResolvedValue(mockData);
|
|
|
|
const result = await getObject({ model: mockModel, id: '123' });
|
|
|
|
expect(mockModel.findById).toHaveBeenCalledWith('123');
|
|
expect(result).toEqual(mockData);
|
|
});
|
|
|
|
it('should return 404 if object not found', async () => {
|
|
mockModel.lean.mockResolvedValue(null);
|
|
|
|
const result = await getObject({ model: mockModel, id: '123' });
|
|
|
|
expect(result).toEqual({ error: 'Object not found.', code: 404 });
|
|
});
|
|
});
|
|
|
|
describe('newObject', () => {
|
|
it('should create a new object', async () => {
|
|
const newData = { name: 'New' };
|
|
const createdData = { _id: '456', ...newData };
|
|
mockModel.create.mockResolvedValue({
|
|
toObject: () => createdData,
|
|
_id: '456',
|
|
});
|
|
|
|
const result = await newObject({ model: mockModel, newData });
|
|
|
|
expect(mockModel.create).toHaveBeenCalledWith(newData);
|
|
expect(result).toEqual(createdData);
|
|
});
|
|
});
|
|
|
|
describe('editObject', () => {
|
|
it('should update an existing object', async () => {
|
|
const id = '123';
|
|
const updateData = { name: 'Updated' };
|
|
const previousData = { _id: id, name: 'Old' };
|
|
|
|
mockModel.lean.mockResolvedValue(previousData);
|
|
|
|
const result = await editObject({ model: mockModel, id, updateData });
|
|
|
|
expect(mockModel.findByIdAndUpdate).toHaveBeenCalledWith(id, updateData);
|
|
expect(result).toEqual({ ...previousData, ...updateData });
|
|
});
|
|
|
|
it('should flatten object id references before updating', async () => {
|
|
const id = '123';
|
|
const fileId = '507f1f77bcf86cd799439012';
|
|
const updateData = { file: { _id: fileId, name: 'part.gcode' } };
|
|
const previousData = { _id: id, name: 'Old' };
|
|
|
|
mockModel.lean.mockResolvedValue(previousData);
|
|
|
|
await editObject({ model: mockModel, id, updateData });
|
|
|
|
expect(mockModel.findByIdAndUpdate).toHaveBeenCalledWith(id, { file: fileId });
|
|
});
|
|
|
|
it('creates an audit log but no user notification for system edits', async () => {
|
|
mockModel.lean.mockResolvedValue({ _id: '123', name: 'Old' });
|
|
|
|
await editObject({
|
|
model: mockModel,
|
|
id: '123',
|
|
updateData: { name: 'Updated' },
|
|
user: 'system',
|
|
});
|
|
|
|
expect(editAuditLog).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
'123',
|
|
'TestModel',
|
|
'system'
|
|
);
|
|
expect(editNotification).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('deleteObject', () => {
|
|
it('should delete an object', async () => {
|
|
const id = '123';
|
|
const mockData = { _id: id, name: 'To be deleted' };
|
|
mockModel.findByIdAndDelete.mockResolvedValue({
|
|
toObject: () => mockData,
|
|
});
|
|
|
|
const result = await deleteObject({ model: mockModel, id });
|
|
|
|
expect(mockModel.findByIdAndDelete).toHaveBeenCalledWith(id);
|
|
expect(result).toEqual({ deleted: true, object: mockData });
|
|
});
|
|
});
|
|
});
|