39948-vm/backend/tests/update-contracts.test.js
2026-06-29 09:38:55 +02:00

487 lines
12 KiB
JavaScript

const assert = require('node:assert/strict');
const test = require('node:test');
const db = require('../src/db/models');
const GenericDBApi = require('../src/db/api/base.api');
const { createEntityService } = require('../src/factories/service.factory');
test('GenericDBApi.update uses object signature and forwards update context', async () => {
const calls = {};
const transaction = { id: 'tx-db-api' };
const currentUser = { id: 'user-1' };
const record = {
async update(payload, options) {
calls.update = { payload, options };
},
async setTags(value, options) {
calls.setTags = { value, options };
},
};
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
async findByPk(id, options) {
calls.findByPk = { id, options };
return record;
},
};
}
static get ASSOCIATIONS() {
return [{ field: 'tags', setter: 'setTags', isArray: true }];
}
}
const result = await TestDBApi.update({
id: 'record-1',
data: { name: 'Updated', tags: ['a', 'b'], skipped: undefined },
currentUser,
transaction,
});
assert.equal(result, record);
assert.deepEqual(calls.findByPk, {
id: 'record-1',
options: { transaction },
});
assert.deepEqual(calls.update, {
payload: {
updatedById: 'user-1',
name: 'Updated',
tags: ['a', 'b'],
},
options: { transaction },
});
assert.deepEqual(calls.setTags, {
value: ['a', 'b'],
options: { transaction },
});
});
test('GenericDBApi.update rejects positional signature', async () => {
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
};
}
}
await assert.rejects(
() => TestDBApi.update('record-1', { name: 'Updated' }, {}),
/DBApi\.update expects an options object/,
);
});
test('GenericDBApi.create uses object signature and forwards create context', async () => {
const calls = {};
const transaction = { id: 'tx-create' };
const currentUser = { id: 'user-1' };
const record = {
id: 'record-1',
async setTags(value, options) {
calls.setTags = { value, options };
},
};
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
async create(payload, options) {
calls.create = { payload, options };
return record;
},
};
}
static get ASSOCIATIONS() {
return [{ field: 'tags', setter: 'setTags', isArray: true }];
}
}
const result = await TestDBApi.create({
data: { name: 'Created', tags: ['a', 'b'] },
currentUser,
transaction,
});
assert.equal(result, record);
assert.deepEqual(calls.create, {
payload: {
name: 'Created',
tags: ['a', 'b'],
importHash: null,
createdById: 'user-1',
updatedById: 'user-1',
},
options: { transaction },
});
assert.deepEqual(calls.setTags, {
value: ['a', 'b'],
options: { transaction },
});
});
test('GenericDBApi.create rejects positional signature', async () => {
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
};
}
}
await assert.rejects(
() => TestDBApi.create({ name: 'Created' }, {}),
/DBApi\.create requires \{ data \}/,
);
});
test('GenericDBApi.partialUpdate uses object signature and only updates defined fields', async () => {
const calls = {};
const transaction = { id: 'tx-partial' };
const currentUser = { id: 'user-1' };
const record = {
async update(payload, options) {
calls.update = { payload, options };
},
};
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
async findByPk(id, options) {
calls.findByPk = { id, options };
return record;
},
};
}
}
const result = await TestDBApi.partialUpdate({
id: 'record-1',
data: { name: 'Updated', skipped: undefined },
currentUser,
transaction,
});
assert.equal(result, record);
assert.deepEqual(calls.findByPk, {
id: 'record-1',
options: { transaction },
});
assert.deepEqual(calls.update, {
payload: {
updatedById: 'user-1',
name: 'Updated',
},
options: { transaction },
});
});
test('GenericDBApi.partialUpdate rejects positional signature', async () => {
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
};
}
}
await assert.rejects(
() => TestDBApi.partialUpdate('record-1', { name: 'Updated' }, {}),
/DBApi\.update expects an options object/,
);
});
test('createEntityService update uses object signature and manages own transaction', async () => {
const originalTransaction = db.sequelize.transaction;
const calls = {};
const transaction = {
async commit() {
calls.committed = true;
},
async rollback() {
calls.rolledBack = true;
},
};
db.sequelize.transaction = async () => transaction;
const DBApi = {
async findBy(where, options) {
calls.findBy = { where, options };
return { id: where.id };
},
async update(options) {
calls.update = options;
return { id: options.id, ...options.data };
},
};
try {
const Service = createEntityService(DBApi, { entityName: 'TestEntity' });
const currentUser = { id: 'user-1' };
const runtimeContext = { environment: 'dev' };
const result = await Service.update({
id: 'record-1',
data: { name: 'Updated' },
currentUser,
runtimeContext,
});
assert.deepEqual(result, { id: 'record-1', name: 'Updated' });
assert.deepEqual(calls.findBy, {
where: { id: 'record-1' },
options: { transaction, runtimeContext },
});
assert.deepEqual(calls.update, {
id: 'record-1',
data: { name: 'Updated' },
currentUser,
transaction,
runtimeContext,
});
assert.equal(calls.committed, true);
assert.equal(calls.rolledBack, undefined);
} finally {
db.sequelize.transaction = originalTransaction;
}
});
test('createEntityService update rejects positional signature', async () => {
const Service = createEntityService(
{
async findBy() {
throw new Error('should not be called');
},
async update() {
throw new Error('should not be called');
},
},
{ entityName: 'TestEntity' },
);
await assert.rejects(
() => Service.update({ name: 'Updated' }, 'record-1', { id: 'user-1' }),
/Service\.update requires \{ id, data \}/,
);
});
test('createEntityService create, deleteByIds, and remove use object signatures', async () => {
const originalTransaction = db.sequelize.transaction;
const calls = {};
const transaction = {
async commit() {
calls.commits = (calls.commits || 0) + 1;
},
async rollback() {
calls.rollbacks = (calls.rollbacks || 0) + 1;
},
};
db.sequelize.transaction = async () => transaction;
const DBApi = {
async create(options) {
calls.create = options;
return { id: 'created-1', ...options.data };
},
async deleteByIds(options) {
calls.deleteByIds = options;
},
async remove(options) {
calls.remove = options;
},
};
try {
const Service = createEntityService(DBApi, { entityName: 'TestEntity' });
const currentUser = { id: 'user-1' };
const runtimeContext = { environment: 'stage' };
const created = await Service.create({
data: { name: 'Created' },
currentUser,
runtimeContext,
});
await Service.deleteByIds({
ids: ['record-1', 'record-2'],
currentUser,
runtimeContext,
});
await Service.remove({
id: 'record-1',
currentUser,
runtimeContext,
});
assert.deepEqual(created, { id: 'created-1', name: 'Created' });
assert.deepEqual(calls.create, {
data: { name: 'Created' },
currentUser,
transaction,
runtimeContext,
});
assert.deepEqual(calls.deleteByIds, {
ids: ['record-1', 'record-2'],
currentUser,
transaction,
runtimeContext,
});
assert.deepEqual(calls.remove, {
id: 'record-1',
currentUser,
transaction,
runtimeContext,
});
assert.equal(calls.commits, 3);
assert.equal(calls.rollbacks, undefined);
} finally {
db.sequelize.transaction = originalTransaction;
}
});
test('createEntityService create, deleteByIds, and remove reject positional signatures', async () => {
const Service = createEntityService(
{
async create() {
throw new Error('should not be called');
},
async deleteByIds() {
throw new Error('should not be called');
},
async remove() {
throw new Error('should not be called');
},
},
{ entityName: 'TestEntity' },
);
await assert.rejects(
() => Service.create({ name: 'Created' }, { id: 'user-1' }),
/Service\.create requires \{ data \}/,
);
await assert.rejects(
() => Service.deleteByIds(['record-1'], { id: 'user-1' }),
/Service\.deleteByIds expects an options object/,
);
await assert.rejects(
() => Service.remove('record-1', { id: 'user-1' }),
/Service\.remove expects an options object/,
);
});
test('GenericDBApi deleteByIds, remove, and findAllAutocomplete use object signatures', async () => {
const calls = {};
const transaction = { id: 'tx-db-api' };
const currentUser = { id: 'user-1' };
const deletedRecords = [
{
id: 'record-1',
async update(payload, options) {
calls.deletedUpdate = { payload, options };
},
async destroy(options) {
calls.deletedDestroy = options;
},
},
];
const removedRecord = {
id: 'record-2',
async update(payload, options) {
calls.removedUpdate = { payload, options };
},
async destroy(options) {
calls.removedDestroy = options;
},
};
const autocompleteRecords = [{ id: 'record-3', name: 'Alpha' }];
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
async findAll(options) {
calls.findAll = options;
return options.attributes ? autocompleteRecords : deletedRecords;
},
async findByPk(id, options) {
calls.findByPk = { id, options };
return removedRecord;
},
};
}
}
const deleted = await TestDBApi.deleteByIds({
ids: ['record-1'],
currentUser,
transaction,
});
const removed = await TestDBApi.remove({
id: 'record-2',
currentUser,
transaction,
});
const autocomplete = await TestDBApi.findAllAutocomplete(
{ query: 'Alpha', limit: 5, offset: 0 },
{ transaction },
);
assert.equal(deleted, deletedRecords);
assert.equal(removed, removedRecord);
assert.deepEqual(calls.deletedUpdate, {
payload: { deletedBy: 'user-1' },
options: { transaction },
});
assert.deepEqual(calls.deletedDestroy, { transaction });
assert.deepEqual(calls.findByPk, {
id: 'record-2',
options: { transaction },
});
assert.deepEqual(calls.removedUpdate, {
payload: { deletedBy: 'user-1' },
options: { transaction },
});
assert.deepEqual(calls.removedDestroy, { transaction });
assert.deepEqual(autocomplete, [{ id: 'record-3', label: 'Alpha' }]);
assert.equal(calls.findAll.limit, 5);
assert.equal(calls.findAll.transaction, transaction);
});
test('GenericDBApi deleteByIds, remove, and findAllAutocomplete reject positional signatures', async () => {
class TestDBApi extends GenericDBApi {
static get MODEL() {
return {
rawAttributes: {},
getTableName: () => 'test_records',
};
}
}
await assert.rejects(
() => TestDBApi.deleteByIds(['record-1'], {}),
/DBApi\.deleteByIds expects an options object/,
);
await assert.rejects(
() => TestDBApi.remove('record-1', {}),
/DBApi\.remove expects an options object/,
);
await assert.rejects(
() => TestDBApi.findAllAutocomplete('Alpha', 5, 0),
/DBApi\.findAllAutocomplete expects an options object/,
);
});