@flusys/nestjs-core 6.0.2 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ const mockInitialize = jest.fn();
2
+ const dataSourceCtor = jest.fn().mockImplementation((options)=>({
3
+ options,
4
+ initialize: mockInitialize
5
+ }));
6
+ jest.mock('typeorm', ()=>({
7
+ DataSource: jest.fn().mockImplementation((options)=>dataSourceCtor(options))
8
+ }));
9
+ jest.mock('typeorm-naming-strategies', ()=>({
10
+ SnakeNamingStrategy: jest.fn().mockImplementation(()=>({
11
+ __brand: 'SnakeNamingStrategy'
12
+ }))
13
+ }));
14
+ import { createMigrationDataSource, initializeDataSource } from './datasource.factory';
15
+ describe('datasource.factory', ()=>{
16
+ const baseConfig = {
17
+ defaultDatabaseConfig: {
18
+ type: 'postgres',
19
+ host: 'localhost',
20
+ port: 5432,
21
+ username: 'root',
22
+ password: 'pw',
23
+ database: 'flusys_default'
24
+ },
25
+ migrationsPath: 'migrations',
26
+ entities: [
27
+ Object
28
+ ]
29
+ };
30
+ beforeEach(()=>{
31
+ jest.clearAllMocks();
32
+ mockInitialize.mockResolvedValue(undefined);
33
+ });
34
+ describe('createMigrationDataSource', ()=>{
35
+ it('should build a DataSource with the resolved connection and a snake naming strategy', ()=>{
36
+ const ds = createMigrationDataSource({
37
+ config: baseConfig
38
+ });
39
+ expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
40
+ type: 'postgres',
41
+ host: 'localhost',
42
+ database: 'flusys_default',
43
+ synchronize: false,
44
+ migrations: [
45
+ 'migrations/default/*{.ts,.js}'
46
+ ]
47
+ }));
48
+ expect(ds.options.namingStrategy).toEqual({
49
+ __brand: 'SnakeNamingStrategy'
50
+ });
51
+ });
52
+ it('should resolve tenant-specific database and migrations path', ()=>{
53
+ const config = {
54
+ ...baseConfig,
55
+ tenants: [
56
+ {
57
+ id: 'tenant-a',
58
+ database: 'db_a'
59
+ }
60
+ ]
61
+ };
62
+ createMigrationDataSource({
63
+ config,
64
+ tenantId: 'tenant-a'
65
+ });
66
+ expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
67
+ database: 'db_a',
68
+ migrations: [
69
+ 'migrations/tenant-a/*{.ts,.js}'
70
+ ]
71
+ }));
72
+ });
73
+ it('should resolve entities via an entity resolver function when provided', ()=>{
74
+ const resolver = jest.fn().mockReturnValue([
75
+ Number,
76
+ String
77
+ ]);
78
+ const config = {
79
+ ...baseConfig,
80
+ entities: resolver
81
+ };
82
+ createMigrationDataSource({
83
+ config
84
+ });
85
+ expect(resolver).toHaveBeenCalled();
86
+ expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
87
+ entities: [
88
+ Number,
89
+ String
90
+ ]
91
+ }));
92
+ });
93
+ });
94
+ describe('initializeDataSource', ()=>{
95
+ it('should create and initialize the DataSource', async ()=>{
96
+ const ds = await initializeDataSource({
97
+ config: baseConfig
98
+ });
99
+ expect(mockInitialize).toHaveBeenCalledTimes(1);
100
+ expect(ds).toBeDefined();
101
+ });
102
+ it('should propagate initialization errors', async ()=>{
103
+ mockInitialize.mockRejectedValue(new Error('connection refused'));
104
+ await expect(initializeDataSource({
105
+ config: baseConfig
106
+ })).rejects.toThrow('connection refused');
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,289 @@
1
+ const mockGetTenantId = jest.fn();
2
+ const mockTryGetValue = jest.fn();
3
+ const mockIsProduction = jest.fn();
4
+ jest.mock('../config', ()=>({
5
+ envConfig: {
6
+ getTenantId: (...args)=>mockGetTenantId(...args),
7
+ tryGetValue: (...args)=>mockTryGetValue(...args),
8
+ isProduction: (...args)=>mockIsProduction(...args)
9
+ }
10
+ }));
11
+ const mockGetDatabaseForTenant = jest.fn();
12
+ const mockGetMigrationsFolderPath = jest.fn();
13
+ jest.mock('../utils', ()=>({
14
+ getDatabaseForTenant: (...args)=>mockGetDatabaseForTenant(...args),
15
+ getMigrationsFolderPath: (...args)=>mockGetMigrationsFolderPath(...args)
16
+ }));
17
+ const mockGenerateMigration = jest.fn();
18
+ const mockRunMigrations = jest.fn();
19
+ const mockRevertMigration = jest.fn();
20
+ const mockResetDatabase = jest.fn();
21
+ const mockMigrationStatus = jest.fn();
22
+ const mockRunForAllTenants = jest.fn();
23
+ jest.mock('./migration.runner', ()=>({
24
+ generateMigration: (...args)=>mockGenerateMigration(...args),
25
+ runMigrations: (...args)=>mockRunMigrations(...args),
26
+ revertMigration: (...args)=>mockRevertMigration(...args),
27
+ resetDatabase: (...args)=>mockResetDatabase(...args),
28
+ migrationStatus: (...args)=>mockMigrationStatus(...args),
29
+ runForAllTenants: (...args)=>mockRunForAllTenants(...args)
30
+ }));
31
+ import { runMigrationCli } from './migration.cli';
32
+ describe('runMigrationCli', ()=>{
33
+ const baseConfig = {
34
+ defaultDatabaseConfig: {
35
+ type: 'postgres',
36
+ database: 'flusys_default'
37
+ },
38
+ migrationsPath: 'migrations',
39
+ entities: []
40
+ };
41
+ let logSpy;
42
+ let errorSpy;
43
+ let exitSpy;
44
+ let originalArgv;
45
+ beforeEach(()=>{
46
+ jest.clearAllMocks();
47
+ originalArgv = process.argv;
48
+ logSpy = jest.spyOn(console, 'log').mockImplementation(()=>undefined);
49
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(()=>undefined);
50
+ exitSpy = jest.spyOn(process, 'exit').mockImplementation(()=>{
51
+ throw new Error('process.exit called');
52
+ });
53
+ mockGetTenantId.mockReturnValue(null);
54
+ mockTryGetValue.mockReturnValue('');
55
+ mockIsProduction.mockReturnValue(false);
56
+ mockGetDatabaseForTenant.mockReturnValue({
57
+ database: 'flusys_default'
58
+ });
59
+ mockGetMigrationsFolderPath.mockReturnValue('migrations/default');
60
+ });
61
+ afterEach(()=>{
62
+ process.argv = originalArgv;
63
+ logSpy.mockRestore();
64
+ errorSpy.mockRestore();
65
+ exitSpy.mockRestore();
66
+ });
67
+ function setArgv(...args) {
68
+ process.argv = [
69
+ 'node',
70
+ 'cli.js',
71
+ ...args
72
+ ];
73
+ }
74
+ it('should print help and return when no command is given', async ()=>{
75
+ setArgv();
76
+ await runMigrationCli(baseConfig);
77
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Migration CLI'));
78
+ expect(mockRunMigrations).not.toHaveBeenCalled();
79
+ });
80
+ it('should run migrations for the default tenant', async ()=>{
81
+ setArgv('run');
82
+ mockRunMigrations.mockResolvedValue({
83
+ success: true,
84
+ migrationsRun: []
85
+ });
86
+ await runMigrationCli(baseConfig);
87
+ expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, undefined);
88
+ expect(exitSpy).not.toHaveBeenCalled();
89
+ });
90
+ it('should exit with code 1 when run fails', async ()=>{
91
+ setArgv('run');
92
+ mockRunMigrations.mockResolvedValue({
93
+ success: false,
94
+ error: 'boom'
95
+ });
96
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
97
+ expect(exitSpy).toHaveBeenCalledWith(1);
98
+ });
99
+ it('should revert the last migration', async ()=>{
100
+ setArgv('revert');
101
+ mockRevertMigration.mockResolvedValue({
102
+ success: true
103
+ });
104
+ await runMigrationCli(baseConfig);
105
+ expect(mockRevertMigration).toHaveBeenCalledWith(baseConfig, undefined);
106
+ });
107
+ it('should exit with code 1 when revert fails', async ()=>{
108
+ setArgv('revert');
109
+ mockRevertMigration.mockResolvedValue({
110
+ success: false,
111
+ error: 'nothing to revert'
112
+ });
113
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
114
+ expect(exitSpy).toHaveBeenCalledWith(1);
115
+ });
116
+ it('should report migration status', async ()=>{
117
+ setArgv('status');
118
+ mockMigrationStatus.mockResolvedValue({
119
+ success: true,
120
+ migrationsRun: [
121
+ 'Up to date'
122
+ ]
123
+ });
124
+ await runMigrationCli(baseConfig);
125
+ expect(mockMigrationStatus).toHaveBeenCalledWith(baseConfig, undefined);
126
+ });
127
+ it('should generate a migration using the --name flag', async ()=>{
128
+ setArgv('generate', '--name=AddProducts');
129
+ mockGenerateMigration.mockResolvedValue(undefined);
130
+ await runMigrationCli(baseConfig);
131
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'AddProducts', undefined, undefined);
132
+ });
133
+ it('should prefer npm_config_name over --name', async ()=>{
134
+ setArgv('generate', '--name=Ignored');
135
+ mockTryGetValue.mockImplementation((key)=>key === 'npm_config_name' ? 'FromNpm' : '');
136
+ mockGenerateMigration.mockResolvedValue(undefined);
137
+ await runMigrationCli(baseConfig);
138
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'FromNpm', undefined, undefined);
139
+ });
140
+ it('should exit with code 1 when generate is missing a name', async ()=>{
141
+ setArgv('generate');
142
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
143
+ expect(exitSpy).toHaveBeenCalledWith(1);
144
+ expect(mockGenerateMigration).not.toHaveBeenCalled();
145
+ });
146
+ it('should pass the resolved tenant id through to sub-commands', async ()=>{
147
+ setArgv('run');
148
+ mockGetTenantId.mockReturnValue('tenant-a');
149
+ mockRunMigrations.mockResolvedValue({
150
+ success: true,
151
+ migrationsRun: []
152
+ });
153
+ await runMigrationCli(baseConfig);
154
+ expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, 'tenant-a');
155
+ });
156
+ it('should reject reset in production without --force', async ()=>{
157
+ setArgv('reset');
158
+ mockIsProduction.mockReturnValue(true);
159
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
160
+ expect(exitSpy).toHaveBeenCalledWith(1);
161
+ expect(mockResetDatabase).not.toHaveBeenCalled();
162
+ });
163
+ it('should allow reset in production when --force is passed', async ()=>{
164
+ setArgv('reset', '--force');
165
+ mockIsProduction.mockReturnValue(true);
166
+ mockResetDatabase.mockResolvedValue({
167
+ success: true
168
+ });
169
+ mockGenerateMigration.mockResolvedValue(undefined);
170
+ mockRunMigrations.mockResolvedValue({
171
+ success: true,
172
+ migrationsRun: []
173
+ });
174
+ await runMigrationCli(baseConfig);
175
+ expect(mockResetDatabase).toHaveBeenCalledWith(baseConfig, undefined);
176
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'Init', undefined, undefined);
177
+ expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, undefined);
178
+ });
179
+ it('should use provided migration name as the reset init migration name', async ()=>{
180
+ setArgv('reset', '--name=InitialSetup');
181
+ mockResetDatabase.mockResolvedValue({
182
+ success: true
183
+ });
184
+ mockGenerateMigration.mockResolvedValue(undefined);
185
+ mockRunMigrations.mockResolvedValue({
186
+ success: true,
187
+ migrationsRun: []
188
+ });
189
+ await runMigrationCli(baseConfig);
190
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'InitialSetup', undefined, undefined);
191
+ });
192
+ it('should exit with code 1 when reset fails', async ()=>{
193
+ setArgv('reset');
194
+ mockResetDatabase.mockResolvedValue({
195
+ success: false,
196
+ error: 'cannot drop'
197
+ });
198
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
199
+ expect(exitSpy).toHaveBeenCalledWith(1);
200
+ expect(mockGenerateMigration).not.toHaveBeenCalled();
201
+ });
202
+ it('should reject unknown commands', async ()=>{
203
+ setArgv('frobnicate');
204
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
205
+ expect(exitSpy).toHaveBeenCalledWith(1);
206
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown command'));
207
+ });
208
+ describe(':all commands', ()=>{
209
+ it('should run for all tenants when multi-tenant mode is enabled', async ()=>{
210
+ setArgv('run:all');
211
+ const multiTenantConfig = {
212
+ ...baseConfig,
213
+ bootstrapAppConfig: {
214
+ databaseMode: 'multi-tenant'
215
+ }
216
+ };
217
+ mockRunForAllTenants.mockResolvedValue([
218
+ {
219
+ success: true
220
+ }
221
+ ]);
222
+ await runMigrationCli(multiTenantConfig);
223
+ expect(mockRunForAllTenants).toHaveBeenCalledWith(multiTenantConfig, 'run');
224
+ });
225
+ it('should reject :all commands when database mode is single', async ()=>{
226
+ setArgv('run:all');
227
+ const singleConfig = {
228
+ ...baseConfig,
229
+ bootstrapAppConfig: {
230
+ databaseMode: 'single'
231
+ }
232
+ };
233
+ await expect(runMigrationCli(singleConfig)).rejects.toThrow('process.exit called');
234
+ expect(exitSpy).toHaveBeenCalledWith(1);
235
+ expect(mockRunForAllTenants).not.toHaveBeenCalled();
236
+ });
237
+ it('should exit with code 1 when any tenant fails during :all commands', async ()=>{
238
+ setArgv('status:all');
239
+ mockRunForAllTenants.mockResolvedValue([
240
+ {
241
+ success: true
242
+ },
243
+ {
244
+ success: false
245
+ }
246
+ ]);
247
+ await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
248
+ expect(exitSpy).toHaveBeenCalledWith(1);
249
+ });
250
+ it('should parse the action prefix from the :all command', async ()=>{
251
+ setArgv('revert:all');
252
+ mockRunForAllTenants.mockResolvedValue([
253
+ {
254
+ success: true
255
+ }
256
+ ]);
257
+ await runMigrationCli(baseConfig);
258
+ expect(mockRunForAllTenants).toHaveBeenCalledWith(baseConfig, 'revert');
259
+ });
260
+ });
261
+ describe('config resolution', ()=>{
262
+ it('should load config from an explicit string path argument', async ()=>{
263
+ setArgv('status');
264
+ const loadedConfig = {
265
+ ...baseConfig,
266
+ migrationsPath: 'custom-migrations'
267
+ };
268
+ jest.doMock(require('path').resolve(process.cwd(), 'custom-config.ts'), ()=>({
269
+ migrationConfig: loadedConfig
270
+ }), {
271
+ virtual: true
272
+ });
273
+ mockMigrationStatus.mockResolvedValue({
274
+ success: true,
275
+ migrationsRun: [
276
+ 'Up to date'
277
+ ]
278
+ });
279
+ await runMigrationCli('custom-config.ts');
280
+ expect(mockMigrationStatus).toHaveBeenCalledWith(loadedConfig, undefined);
281
+ });
282
+ it('should exit with code 1 and show help when no config can be resolved', async ()=>{
283
+ setArgv('status');
284
+ await expect(runMigrationCli()).rejects.toThrow('process.exit called');
285
+ expect(exitSpy).toHaveBeenCalledWith(1);
286
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Config not found'));
287
+ });
288
+ });
289
+ });
@@ -0,0 +1,264 @@
1
+ const mockExistsSync = jest.fn();
2
+ const mockMkdirSync = jest.fn();
3
+ const mockReaddirSync = jest.fn();
4
+ const mockRmSync = jest.fn();
5
+ const mockExecSync = jest.fn();
6
+ jest.mock('fs', ()=>({
7
+ existsSync: (...args)=>mockExistsSync(...args),
8
+ mkdirSync: (...args)=>mockMkdirSync(...args),
9
+ readdirSync: (...args)=>mockReaddirSync(...args),
10
+ rmSync: (...args)=>mockRmSync(...args)
11
+ }));
12
+ jest.mock('child_process', ()=>({
13
+ execSync: (...args)=>mockExecSync(...args)
14
+ }));
15
+ jest.mock('../config', ()=>({
16
+ envConfig: {
17
+ getEnv: jest.fn().mockReturnValue({
18
+ NODE_ENV: 'test'
19
+ })
20
+ }
21
+ }));
22
+ const mockRunMigrations = jest.fn();
23
+ const mockUndoLastMigration = jest.fn();
24
+ const mockShowMigrations = jest.fn();
25
+ const mockDestroy = jest.fn();
26
+ const mockDsInitialize = jest.fn();
27
+ const mockAdminInitialize = jest.fn();
28
+ const mockAdminQuery = jest.fn();
29
+ const mockAdminDestroy = jest.fn();
30
+ jest.mock('./datasource.factory', ()=>({
31
+ initializeDataSource: jest.fn()
32
+ }));
33
+ jest.mock('typeorm', ()=>({
34
+ DataSource: jest.fn().mockImplementation(()=>({
35
+ initialize: mockAdminInitialize,
36
+ query: mockAdminQuery,
37
+ destroy: mockAdminDestroy
38
+ }))
39
+ }));
40
+ import { ensureMigrationsFolder, generateMigration, runMigrations, revertMigration, resetDatabase, migrationStatus, runForAllTenants } from './migration.runner';
41
+ import { initializeDataSource } from './datasource.factory';
42
+ describe('migration.runner', ()=>{
43
+ const baseConfig = {
44
+ defaultDatabaseConfig: {
45
+ type: 'postgres',
46
+ database: 'flusys_default'
47
+ },
48
+ migrationsPath: 'migrations',
49
+ entities: []
50
+ };
51
+ let logSpy;
52
+ let errorSpy;
53
+ beforeEach(()=>{
54
+ jest.clearAllMocks();
55
+ logSpy = jest.spyOn(console, 'log').mockImplementation(()=>undefined);
56
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(()=>undefined);
57
+ initializeDataSource.mockResolvedValue({
58
+ runMigrations: mockRunMigrations,
59
+ undoLastMigration: mockUndoLastMigration,
60
+ showMigrations: mockShowMigrations,
61
+ destroy: mockDestroy,
62
+ initialize: mockDsInitialize
63
+ });
64
+ });
65
+ afterEach(()=>{
66
+ logSpy.mockRestore();
67
+ errorSpy.mockRestore();
68
+ });
69
+ describe('ensureMigrationsFolder', ()=>{
70
+ it('should create the folder when it does not exist', ()=>{
71
+ mockExistsSync.mockReturnValue(false);
72
+ const folder = ensureMigrationsFolder('migrations', 'tenant-a');
73
+ expect(folder).toBe('migrations/tenant-a');
74
+ expect(mockMkdirSync).toHaveBeenCalledWith('migrations/tenant-a', {
75
+ recursive: true
76
+ });
77
+ });
78
+ it('should not create the folder when it already exists', ()=>{
79
+ mockExistsSync.mockReturnValue(true);
80
+ ensureMigrationsFolder('migrations');
81
+ expect(mockMkdirSync).not.toHaveBeenCalled();
82
+ });
83
+ });
84
+ describe('generateMigration', ()=>{
85
+ it('should shell out to the typeorm CLI with the resolved datasource path', async ()=>{
86
+ mockExistsSync.mockImplementation((p)=>p.includes('flusysnest'));
87
+ await generateMigration(baseConfig, 'AddProductTable', 'tenant-a');
88
+ expect(mockExecSync).toHaveBeenCalledTimes(1);
89
+ const [command, options] = mockExecSync.mock.calls[0];
90
+ expect(command).toContain('migration:generate');
91
+ expect(command).toContain('migrations/tenant-a/AddProductTable');
92
+ expect(command).toContain('projects/flusysnest/src/persistence/migration.config.ts');
93
+ expect(options.env.TENANT_ID).toBe('tenant-a');
94
+ });
95
+ it('should use an explicit datasource path when provided', async ()=>{
96
+ mockExistsSync.mockReturnValue(false);
97
+ await generateMigration(baseConfig, 'AddProductTable', undefined, 'custom/datasource.ts');
98
+ const [command] = mockExecSync.mock.calls[0];
99
+ expect(command).toContain('-d "custom/datasource.ts"');
100
+ });
101
+ it('should throw when no datasource file can be found and none is specified', async ()=>{
102
+ mockExistsSync.mockReturnValue(false);
103
+ await expect(generateMigration(baseConfig, 'AddProductTable')).rejects.toThrow('Could not find datasource file. Specify with --datasource=<path>');
104
+ });
105
+ });
106
+ describe('runMigrations', ()=>{
107
+ it('should report success with the list of executed migrations', async ()=>{
108
+ mockRunMigrations.mockResolvedValue([
109
+ {
110
+ name: 'M1'
111
+ },
112
+ {
113
+ name: 'M2'
114
+ }
115
+ ]);
116
+ const result = await runMigrations(baseConfig);
117
+ expect(result).toEqual({
118
+ tenantId: undefined,
119
+ database: 'flusys_default',
120
+ success: true,
121
+ migrationsRun: [
122
+ 'M1',
123
+ 'M2'
124
+ ]
125
+ });
126
+ expect(mockDestroy).toHaveBeenCalled();
127
+ });
128
+ it('should report no pending migrations when none run', async ()=>{
129
+ mockRunMigrations.mockResolvedValue([]);
130
+ const result = await runMigrations(baseConfig);
131
+ expect(result.migrationsRun).toEqual([]);
132
+ expect(result.success).toBe(true);
133
+ });
134
+ it('should capture errors and mark the result as failed', async ()=>{
135
+ initializeDataSource.mockRejectedValue(new Error('connection refused'));
136
+ const result = await runMigrations(baseConfig, 'tenant-a');
137
+ expect(result).toEqual({
138
+ tenantId: 'tenant-a',
139
+ database: 'flusys_default',
140
+ success: false,
141
+ error: 'connection refused'
142
+ });
143
+ expect(errorSpy).toHaveBeenCalled();
144
+ });
145
+ });
146
+ describe('revertMigration', ()=>{
147
+ it('should undo the last migration and close the connection', async ()=>{
148
+ const result = await revertMigration(baseConfig);
149
+ expect(mockUndoLastMigration).toHaveBeenCalled();
150
+ expect(mockDestroy).toHaveBeenCalled();
151
+ expect(result.success).toBe(true);
152
+ });
153
+ it('should capture revert errors', async ()=>{
154
+ mockUndoLastMigration.mockRejectedValue(new Error('no migrations to revert'));
155
+ const result = await revertMigration(baseConfig);
156
+ expect(result.success).toBe(false);
157
+ expect(result.error).toBe('no migrations to revert');
158
+ });
159
+ });
160
+ describe('migrationStatus', ()=>{
161
+ it('should report pending status', async ()=>{
162
+ mockShowMigrations.mockResolvedValue(true);
163
+ const result = await migrationStatus(baseConfig);
164
+ expect(result.migrationsRun).toEqual([
165
+ 'Has pending'
166
+ ]);
167
+ });
168
+ it('should report up-to-date status', async ()=>{
169
+ mockShowMigrations.mockResolvedValue(false);
170
+ const result = await migrationStatus(baseConfig);
171
+ expect(result.migrationsRun).toEqual([
172
+ 'Up to date'
173
+ ]);
174
+ });
175
+ });
176
+ describe('resetDatabase', ()=>{
177
+ it('should remove migration files then drop/recreate a postgres database', async ()=>{
178
+ mockExistsSync.mockReturnValue(true);
179
+ mockReaddirSync.mockReturnValue([
180
+ '1-init.ts',
181
+ '2-add.js',
182
+ 'readme.md'
183
+ ]);
184
+ mockAdminQuery.mockResolvedValue(undefined);
185
+ const config = {
186
+ defaultDatabaseConfig: {
187
+ type: 'postgres',
188
+ host: 'localhost',
189
+ port: 5432,
190
+ username: 'root',
191
+ password: 'pw',
192
+ database: 'flusys_default'
193
+ },
194
+ migrationsPath: 'migrations',
195
+ entities: []
196
+ };
197
+ const result = await resetDatabase(config);
198
+ expect(mockRmSync).toHaveBeenCalledTimes(2);
199
+ expect(mockAdminQuery).toHaveBeenCalledWith(expect.stringContaining('DROP DATABASE IF EXISTS "flusys_default"'));
200
+ expect(mockAdminDestroy).toHaveBeenCalled();
201
+ expect(result.success).toBe(true);
202
+ });
203
+ it('should delete the sqlite file directly for sqlite databases', async ()=>{
204
+ mockExistsSync.mockReturnValue(true);
205
+ mockReaddirSync.mockReturnValue([]);
206
+ const config = {
207
+ defaultDatabaseConfig: {
208
+ type: 'sqlite',
209
+ database: 'flusys.sqlite'
210
+ },
211
+ migrationsPath: 'migrations',
212
+ entities: []
213
+ };
214
+ const result = await resetDatabase(config);
215
+ expect(mockRmSync).toHaveBeenCalledWith('flusys.sqlite');
216
+ expect(mockAdminInitialize).not.toHaveBeenCalled();
217
+ expect(result.success).toBe(true);
218
+ });
219
+ it('should fail for unsupported database types', async ()=>{
220
+ mockExistsSync.mockReturnValue(false);
221
+ const config = {
222
+ defaultDatabaseConfig: {
223
+ type: 'mssql',
224
+ database: 'flusys'
225
+ },
226
+ migrationsPath: 'migrations',
227
+ entities: []
228
+ };
229
+ const result = await resetDatabase(config);
230
+ expect(result.success).toBe(false);
231
+ expect(result.error).toContain("Database reset is not supported for type 'mssql'");
232
+ });
233
+ });
234
+ describe('runForAllTenants', ()=>{
235
+ it('should run the given command across all configured tenants and summarize results', async ()=>{
236
+ mockRunMigrations.mockResolvedValue([
237
+ {
238
+ name: 'M1'
239
+ }
240
+ ]);
241
+ const config = {
242
+ ...baseConfig,
243
+ tenants: [
244
+ {
245
+ id: 'tenant-a',
246
+ database: 'db_a'
247
+ },
248
+ {
249
+ id: 'tenant-b',
250
+ database: 'db_b'
251
+ }
252
+ ]
253
+ };
254
+ const results = await runForAllTenants(config, 'run');
255
+ expect(results).toHaveLength(2);
256
+ expect(results.every((r)=>r.success)).toBe(true);
257
+ expect(initializeDataSource).toHaveBeenCalledTimes(2);
258
+ });
259
+ it('should return an empty result set when no tenants are configured', async ()=>{
260
+ const results = await runForAllTenants(baseConfig, 'status');
261
+ expect(results).toEqual([]);
262
+ });
263
+ });
264
+ });