tududi/backend/tests/unit/utils/migration-utils.test.js
Chris 3486541272
Add comprehensive LLM development documentation (#939)
* Increase coverage

* Add comprehensive LLM development documentation

- Add CLAUDE.md as main documentation index
- Create 8 detailed documentation files in docs/:
  - architecture.md: Tech stack, data models, auth system
  - directory-structure.md: Complete file tree with paths
  - backend-patterns.md: Module architecture and patterns
  - database.md: Models, migrations, and workflows
  - development-workflow.md: Setup and daily development
  - code-conventions.md: Style guide and best practices
  - testing.md: Test organization and patterns
  - common-tasks.md: How-to guides for frequent tasks
- Update .gitignore to allow project-level CLAUDE.md
- 4,285 lines of comprehensive documentation
- Organized for easy navigation with cross-links
- LLM-optimized with absolute paths and code examples

* fixup! Add comprehensive LLM development documentation
2026-03-14 02:54:59 +02:00

360 lines
13 KiB
JavaScript

const {
safeAddColumns,
safeCreateTable,
safeAddIndex,
safeRemoveColumn,
safeChangeColumn,
} = require('../../../utils/migration-utils');
describe('migration-utils', () => {
let queryInterface;
beforeEach(() => {
queryInterface = {
showAllTables: jest.fn(),
describeTable: jest.fn(),
addColumn: jest.fn(),
removeColumn: jest.fn(),
createTable: jest.fn(),
showIndex: jest.fn(),
addIndex: jest.fn(),
changeColumn: jest.fn(),
sequelize: {
getDialect: jest.fn().mockReturnValue('postgres'),
query: jest.fn(),
},
};
});
describe('safeAddColumns', () => {
it('should skip when table does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue(['other_table']);
await safeAddColumns(queryInterface, 'missing_table', [
{ name: 'col1', definition: { type: 'TEXT' } },
]);
expect(queryInterface.addColumn).not.toHaveBeenCalled();
});
it('should add column when it does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({
id: {},
title: {},
});
const definition = { type: 'TEXT' };
await safeAddColumns(queryInterface, 'tasks', [
{ name: 'description', definition },
]);
expect(queryInterface.addColumn).toHaveBeenCalledWith(
'tasks',
'description',
definition
);
});
it('should skip column when it already exists', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({
id: {},
title: {},
});
await safeAddColumns(queryInterface, 'tasks', [
{ name: 'title', definition: { type: 'TEXT' } },
]);
expect(queryInterface.addColumn).not.toHaveBeenCalled();
});
it('should add multiple columns, skipping existing ones', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({
id: {},
title: {},
});
await safeAddColumns(queryInterface, 'tasks', [
{ name: 'title', definition: { type: 'TEXT' } }, // exists
{ name: 'priority', definition: { type: 'INTEGER' } }, // new
{ name: 'due_date', definition: { type: 'DATE' } }, // new
]);
expect(queryInterface.addColumn).toHaveBeenCalledTimes(2);
expect(queryInterface.addColumn).toHaveBeenCalledWith(
'tasks',
'priority',
{ type: 'INTEGER' }
);
expect(queryInterface.addColumn).toHaveBeenCalledWith(
'tasks',
'due_date',
{ type: 'DATE' }
);
});
it('should re-throw errors from addColumn', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({ id: {} });
queryInterface.addColumn.mockRejectedValue(new Error('DB error'));
await expect(
safeAddColumns(queryInterface, 'tasks', [
{ name: 'col', definition: { type: 'TEXT' } },
])
).rejects.toThrow('DB error');
});
});
describe('safeCreateTable', () => {
it('should create table when it does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue([]);
const definition = { id: { type: 'INTEGER', primaryKey: true } };
await safeCreateTable(queryInterface, 'new_table', definition);
expect(queryInterface.createTable).toHaveBeenCalledWith(
'new_table',
definition
);
});
it('should skip when table already exists', async () => {
queryInterface.showAllTables.mockResolvedValue(['existing_table']);
await safeCreateTable(queryInterface, 'existing_table', {});
expect(queryInterface.createTable).not.toHaveBeenCalled();
});
it('should re-throw errors from createTable', async () => {
queryInterface.showAllTables.mockResolvedValue([]);
queryInterface.createTable.mockRejectedValue(
new Error('Create failed')
);
await expect(
safeCreateTable(queryInterface, 'fail_table', {})
).rejects.toThrow('Create failed');
});
});
describe('safeAddIndex', () => {
it('should skip when table does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue([]);
await safeAddIndex(queryInterface, 'missing_table', ['col1']);
expect(queryInterface.addIndex).not.toHaveBeenCalled();
});
it('should add index when it does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.showIndex.mockResolvedValue([]);
await safeAddIndex(queryInterface, 'tasks', ['user_id'], {
name: 'idx_user',
});
expect(queryInterface.addIndex).toHaveBeenCalledWith(
'tasks',
['user_id'],
{ name: 'idx_user' }
);
});
it('should skip when a matching index already exists', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.showIndex.mockResolvedValue([
{ fields: [{ attribute: 'user_id' }] },
]);
await safeAddIndex(queryInterface, 'tasks', ['user_id']);
expect(queryInterface.addIndex).not.toHaveBeenCalled();
});
it('should not throw on addIndex failure (swallows error)', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.showIndex.mockResolvedValue([]);
queryInterface.addIndex.mockRejectedValue(new Error('Index error'));
// safeAddIndex swallows errors (no throw in the catch)
await expect(
safeAddIndex(queryInterface, 'tasks', ['col'])
).resolves.toBeUndefined();
});
});
describe('safeRemoveColumn', () => {
it('should skip when column does not exist', async () => {
queryInterface.describeTable.mockResolvedValue({
id: {},
title: {},
});
await safeRemoveColumn(queryInterface, 'tasks', 'nonexistent');
expect(queryInterface.removeColumn).not.toHaveBeenCalled();
});
it('should remove column on non-SQLite dialect', async () => {
queryInterface.describeTable.mockResolvedValue({
id: {},
title: {},
old_col: {},
});
queryInterface.sequelize.getDialect.mockReturnValue('postgres');
await safeRemoveColumn(queryInterface, 'tasks', 'old_col');
expect(queryInterface.removeColumn).toHaveBeenCalledWith(
'tasks',
'old_col'
);
});
it('should use table recreation strategy on SQLite', async () => {
queryInterface.describeTable.mockResolvedValue({
id: {
type: 'INTEGER',
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
title: { type: 'TEXT', allowNull: true },
remove_me: { type: 'TEXT', allowNull: true },
});
queryInterface.sequelize.getDialect.mockReturnValue('sqlite');
await safeRemoveColumn(queryInterface, 'tasks', 'remove_me');
// Should have called multiple queries for SQLite recreation
const calls = queryInterface.sequelize.query.mock.calls.map(
(c) => c[0]
);
expect(
calls.some((q) => q.includes('PRAGMA foreign_keys = OFF'))
).toBe(true);
expect(
calls.some((q) => q.includes('CREATE TABLE tasks_new'))
).toBe(true);
expect(calls.some((q) => q.includes('INSERT INTO tasks_new'))).toBe(
true
);
expect(calls.some((q) => q.includes('DROP TABLE tasks'))).toBe(
true
);
expect(
calls.some((q) =>
q.includes('ALTER TABLE tasks_new RENAME TO tasks')
)
).toBe(true);
expect(
calls.some((q) => q.includes('PRAGMA foreign_keys = ON'))
).toBe(true);
// remove_me should not appear in CREATE TABLE
const createCall = calls.find((q) =>
q.includes('CREATE TABLE tasks_new')
);
expect(createCall).not.toContain('remove_me');
});
it('should re-throw errors', async () => {
queryInterface.describeTable.mockRejectedValue(
new Error('Describe failed')
);
await expect(
safeRemoveColumn(queryInterface, 'tasks', 'col')
).rejects.toThrow('Describe failed');
});
});
describe('safeChangeColumn', () => {
it('should skip when table does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue([]);
await safeChangeColumn(queryInterface, 'missing', 'col', {
type: 'TEXT',
});
expect(queryInterface.changeColumn).not.toHaveBeenCalled();
});
it('should skip when column does not exist', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({ id: {} });
await safeChangeColumn(queryInterface, 'tasks', 'nonexistent', {
type: 'TEXT',
});
expect(queryInterface.changeColumn).not.toHaveBeenCalled();
});
it('should change column on non-SQLite dialect', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({
id: {},
title: { type: 'TEXT', allowNull: true },
});
queryInterface.sequelize.getDialect.mockReturnValue('postgres');
const newDef = { type: 'VARCHAR(255)', allowNull: false };
await safeChangeColumn(queryInterface, 'tasks', 'title', newDef);
expect(queryInterface.changeColumn).toHaveBeenCalledWith(
'tasks',
'title',
newDef
);
});
it('should use table recreation on SQLite', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({
id: {
type: 'INTEGER',
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
title: { type: 'TEXT', allowNull: true },
});
queryInterface.showIndex.mockResolvedValue([]);
queryInterface.sequelize.getDialect.mockReturnValue('sqlite');
await safeChangeColumn(queryInterface, 'tasks', 'title', {
type: { toSql: () => 'VARCHAR(255)' },
allowNull: false,
});
const calls = queryInterface.sequelize.query.mock.calls.map(
(c) => c[0]
);
expect(
calls.some((q) => q.includes('CREATE TABLE tasks_new'))
).toBe(true);
const createCall = calls.find((q) =>
q.includes('CREATE TABLE tasks_new')
);
expect(createCall).toContain('VARCHAR(255)');
expect(createCall).toContain('NOT NULL');
});
it('should re-throw errors', async () => {
queryInterface.showAllTables.mockResolvedValue(['tasks']);
queryInterface.describeTable.mockResolvedValue({ id: {}, col: {} });
queryInterface.sequelize.getDialect.mockReturnValue('postgres');
queryInterface.changeColumn.mockRejectedValue(
new Error('Change failed')
);
await expect(
safeChangeColumn(queryInterface, 'tasks', 'col', {
type: 'TEXT',
})
).rejects.toThrow('Change failed');
});
});
});