@hazeljs/cron 0.2.0-beta.55 → 0.2.0-beta.56

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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cron.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.test.d.ts","sourceRoot":"","sources":["../src/cron.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ /// <reference types="jest" />
3
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
6
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
7
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
8
+ };
9
+ var __metadata = (this && this.__metadata) || function (k, v) {
10
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ jest.mock('@hazeljs/core', () => ({
14
+ __esModule: true,
15
+ Service: () => () => undefined,
16
+ HazelModule: () => () => undefined,
17
+ Container: { getInstance: jest.fn() },
18
+ logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
19
+ default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
20
+ }));
21
+ const core_1 = require("@hazeljs/core");
22
+ const cron_service_1 = require("./cron.service");
23
+ const cron_module_1 = require("./cron.module");
24
+ const cron_decorator_1 = require("./cron.decorator");
25
+ const cron_types_1 = require("./cron.types");
26
+ // Use a minute-level cron expression that won't fire during tests
27
+ const EVERY_MINUTE = '* * * * *';
28
+ describe('CronService', () => {
29
+ let service;
30
+ beforeEach(() => {
31
+ service = new cron_service_1.CronService();
32
+ });
33
+ afterEach(() => {
34
+ service.clearAll();
35
+ });
36
+ describe('registerJob()', () => {
37
+ it('registers a job and increments count', () => {
38
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
39
+ expect(service.getJobCount()).toBe(1);
40
+ });
41
+ it('replaces an existing job with the same name', () => {
42
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
43
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
44
+ expect(service.getJobCount()).toBe(1);
45
+ });
46
+ it('does not auto-start when enabled is false', () => {
47
+ service.registerJob('job1', EVERY_MINUTE, jest.fn(), {
48
+ cronTime: EVERY_MINUTE,
49
+ enabled: false,
50
+ });
51
+ const status = service.getJobStatus('job1');
52
+ expect(status).toBeDefined();
53
+ expect(status?.enabled).toBe(false);
54
+ });
55
+ it('throws for invalid cron expression', () => {
56
+ expect(() => service.registerJob('bad', 'not-a-cron', jest.fn())).toThrow('Invalid cron expression');
57
+ });
58
+ it('registers multiple independent jobs', () => {
59
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
60
+ service.registerJob('job2', EVERY_MINUTE, jest.fn());
61
+ service.registerJob('job3', EVERY_MINUTE, jest.fn());
62
+ expect(service.getJobCount()).toBe(3);
63
+ });
64
+ });
65
+ describe('deleteJob()', () => {
66
+ it('returns true and removes the job', () => {
67
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
68
+ expect(service.deleteJob('job1')).toBe(true);
69
+ expect(service.getJobCount()).toBe(0);
70
+ });
71
+ it('returns false for unknown job', () => {
72
+ expect(service.deleteJob('unknown')).toBe(false);
73
+ });
74
+ });
75
+ describe('startJob() / stopJob()', () => {
76
+ it('startJob returns true for existing job', () => {
77
+ service.registerJob('job1', EVERY_MINUTE, jest.fn(), {
78
+ cronTime: EVERY_MINUTE,
79
+ enabled: false,
80
+ });
81
+ expect(service.startJob('job1')).toBe(true);
82
+ });
83
+ it('startJob returns false for unknown job', () => {
84
+ expect(service.startJob('unknown')).toBe(false);
85
+ });
86
+ it('stopJob returns true for existing job', () => {
87
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
88
+ expect(service.stopJob('job1')).toBe(true);
89
+ });
90
+ it('stopJob returns false for unknown job', () => {
91
+ expect(service.stopJob('unknown')).toBe(false);
92
+ });
93
+ });
94
+ describe('enableJob() / disableJob()', () => {
95
+ it('enableJob returns true for existing job', () => {
96
+ service.registerJob('job1', EVERY_MINUTE, jest.fn(), {
97
+ cronTime: EVERY_MINUTE,
98
+ enabled: false,
99
+ });
100
+ expect(service.enableJob('job1')).toBe(true);
101
+ });
102
+ it('enableJob returns false for unknown job', () => {
103
+ expect(service.enableJob('unknown')).toBe(false);
104
+ });
105
+ it('disableJob returns true for existing job', () => {
106
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
107
+ expect(service.disableJob('job1')).toBe(true);
108
+ });
109
+ it('disableJob returns false for unknown job', () => {
110
+ expect(service.disableJob('unknown')).toBe(false);
111
+ });
112
+ });
113
+ describe('getJobStatus()', () => {
114
+ it('returns status for registered job', () => {
115
+ service.registerJob('job1', EVERY_MINUTE, jest.fn());
116
+ const status = service.getJobStatus('job1');
117
+ expect(status).toBeDefined();
118
+ expect(status?.name).toBe('job1');
119
+ expect(status?.runCount).toBe(0);
120
+ expect(status?.enabled).toBe(true);
121
+ });
122
+ it('returns undefined for unknown job', () => {
123
+ expect(service.getJobStatus('unknown')).toBeUndefined();
124
+ });
125
+ });
126
+ describe('getAllJobStatuses()', () => {
127
+ it('returns empty array when no jobs', () => {
128
+ expect(service.getAllJobStatuses()).toEqual([]);
129
+ });
130
+ it('returns all job statuses', () => {
131
+ service.registerJob('j1', EVERY_MINUTE, jest.fn());
132
+ service.registerJob('j2', EVERY_MINUTE, jest.fn());
133
+ const statuses = service.getAllJobStatuses();
134
+ expect(statuses).toHaveLength(2);
135
+ expect(statuses.map((s) => s.name)).toContain('j1');
136
+ expect(statuses.map((s) => s.name)).toContain('j2');
137
+ });
138
+ });
139
+ describe('stopAll() / startAll() / clearAll()', () => {
140
+ it('stopAll stops all registered jobs', () => {
141
+ service.registerJob('j1', EVERY_MINUTE, jest.fn());
142
+ service.registerJob('j2', EVERY_MINUTE, jest.fn());
143
+ expect(() => service.stopAll()).not.toThrow();
144
+ });
145
+ it('startAll starts all registered jobs', () => {
146
+ service.registerJob('j1', EVERY_MINUTE, jest.fn(), {
147
+ cronTime: EVERY_MINUTE,
148
+ enabled: false,
149
+ });
150
+ expect(() => service.startAll()).not.toThrow();
151
+ });
152
+ it('clearAll removes all jobs', () => {
153
+ service.registerJob('j1', EVERY_MINUTE, jest.fn());
154
+ service.registerJob('j2', EVERY_MINUTE, jest.fn());
155
+ service.clearAll();
156
+ expect(service.getJobCount()).toBe(0);
157
+ });
158
+ });
159
+ describe('getJobCount()', () => {
160
+ it('returns 0 when no jobs registered', () => {
161
+ expect(service.getJobCount()).toBe(0);
162
+ });
163
+ it('returns correct count after adding/removing jobs', () => {
164
+ service.registerJob('j1', EVERY_MINUTE, jest.fn());
165
+ service.registerJob('j2', EVERY_MINUTE, jest.fn());
166
+ expect(service.getJobCount()).toBe(2);
167
+ service.deleteJob('j1');
168
+ expect(service.getJobCount()).toBe(1);
169
+ });
170
+ });
171
+ describe('job options: runOnInit', () => {
172
+ it('executes callback immediately when runOnInit is true', async () => {
173
+ const callback = jest.fn().mockResolvedValue(undefined);
174
+ service.registerJob('runNow', EVERY_MINUTE, callback, {
175
+ cronTime: EVERY_MINUTE,
176
+ runOnInit: true,
177
+ });
178
+ // Allow the async execute() to run
179
+ await new Promise((r) => setTimeout(r, 20));
180
+ expect(callback).toHaveBeenCalled();
181
+ });
182
+ });
183
+ describe('job options: onComplete / onError', () => {
184
+ it('calls onComplete after successful execution (via runOnInit)', async () => {
185
+ const onComplete = jest.fn();
186
+ service.registerJob('complete-job', EVERY_MINUTE, jest.fn().mockResolvedValue(undefined), {
187
+ cronTime: EVERY_MINUTE,
188
+ runOnInit: true,
189
+ onComplete,
190
+ });
191
+ await new Promise((r) => setTimeout(r, 20));
192
+ expect(onComplete).toHaveBeenCalled();
193
+ });
194
+ it('calls onError when callback throws (via runOnInit)', async () => {
195
+ const onError = jest.fn();
196
+ service.registerJob('error-job', EVERY_MINUTE, jest.fn().mockRejectedValue(new Error('fail')), {
197
+ cronTime: EVERY_MINUTE,
198
+ runOnInit: true,
199
+ onError,
200
+ });
201
+ await new Promise((r) => setTimeout(r, 20));
202
+ expect(onError).toHaveBeenCalledWith(expect.any(Error));
203
+ });
204
+ });
205
+ describe('startJob on disabled job', () => {
206
+ it('warns but does not crash when starting a disabled job', () => {
207
+ service.registerJob('disabled-job', EVERY_MINUTE, jest.fn(), {
208
+ cronTime: EVERY_MINUTE,
209
+ enabled: false,
210
+ });
211
+ // startJob calls job.start() which logs a warn since job is disabled
212
+ expect(() => service.startJob('disabled-job')).not.toThrow();
213
+ });
214
+ });
215
+ describe('starting an already-started job', () => {
216
+ it('does not throw when starting a job that is already running', () => {
217
+ service.registerJob('already-running', EVERY_MINUTE, jest.fn());
218
+ // job is already started by registerJob
219
+ expect(() => service.startJob('already-running')).not.toThrow();
220
+ });
221
+ });
222
+ });
223
+ describe('Cron decorator', () => {
224
+ it('attaches metadata to the class', () => {
225
+ class MyService {
226
+ doWork() {
227
+ // noop
228
+ }
229
+ }
230
+ __decorate([
231
+ (0, cron_decorator_1.Cron)({ cronTime: EVERY_MINUTE, name: 'myJob' }),
232
+ __metadata("design:type", Function),
233
+ __metadata("design:paramtypes", []),
234
+ __metadata("design:returntype", void 0)
235
+ ], MyService.prototype, "doWork", null);
236
+ const metadata = Reflect.getMetadata(cron_decorator_1.CRON_METADATA_KEY, MyService);
237
+ expect(metadata).toHaveLength(1);
238
+ expect(metadata[0].methodName).toBe('doWork');
239
+ expect(metadata[0].options.name).toBe('myJob');
240
+ expect(metadata[0].options.cronTime).toBe(EVERY_MINUTE);
241
+ });
242
+ it('uses default name based on class and method when name is not provided', () => {
243
+ class AnotherService {
244
+ handleTask() {
245
+ // noop
246
+ }
247
+ }
248
+ __decorate([
249
+ (0, cron_decorator_1.Cron)({ cronTime: EVERY_MINUTE }),
250
+ __metadata("design:type", Function),
251
+ __metadata("design:paramtypes", []),
252
+ __metadata("design:returntype", void 0)
253
+ ], AnotherService.prototype, "handleTask", null);
254
+ const metadata = Reflect.getMetadata(cron_decorator_1.CRON_METADATA_KEY, AnotherService);
255
+ expect(metadata[0].options.name).toBe('AnotherService.handleTask');
256
+ });
257
+ it('accumulates metadata for multiple methods', () => {
258
+ class MultiService {
259
+ firstJob() {
260
+ // noop
261
+ }
262
+ secondJob() {
263
+ // noop
264
+ }
265
+ }
266
+ __decorate([
267
+ (0, cron_decorator_1.Cron)({ cronTime: EVERY_MINUTE, name: 'first' }),
268
+ __metadata("design:type", Function),
269
+ __metadata("design:paramtypes", []),
270
+ __metadata("design:returntype", void 0)
271
+ ], MultiService.prototype, "firstJob", null);
272
+ __decorate([
273
+ (0, cron_decorator_1.Cron)({ cronTime: EVERY_MINUTE, name: 'second' }),
274
+ __metadata("design:type", Function),
275
+ __metadata("design:paramtypes", []),
276
+ __metadata("design:returntype", void 0)
277
+ ], MultiService.prototype, "secondJob", null);
278
+ const metadata = Reflect.getMetadata(cron_decorator_1.CRON_METADATA_KEY, MultiService);
279
+ expect(metadata).toHaveLength(2);
280
+ const names = metadata.map((m) => m.options.name);
281
+ expect(names).toContain('first');
282
+ expect(names).toContain('second');
283
+ });
284
+ });
285
+ describe('getCronMetadata()', () => {
286
+ it('returns empty array for class with no @Cron decorators', () => {
287
+ class Plain {
288
+ doThing() {
289
+ // noop
290
+ }
291
+ }
292
+ const instance = new Plain();
293
+ expect((0, cron_decorator_1.getCronMetadata)(instance)).toEqual([]);
294
+ });
295
+ it('returns metadata for class with @Cron decorators', () => {
296
+ class Scheduled {
297
+ job() {
298
+ // noop
299
+ }
300
+ }
301
+ __decorate([
302
+ (0, cron_decorator_1.Cron)({ cronTime: EVERY_MINUTE }),
303
+ __metadata("design:type", Function),
304
+ __metadata("design:paramtypes", []),
305
+ __metadata("design:returntype", void 0)
306
+ ], Scheduled.prototype, "job", null);
307
+ const instance = new Scheduled();
308
+ const meta = (0, cron_decorator_1.getCronMetadata)(instance);
309
+ expect(meta).toHaveLength(1);
310
+ expect(meta[0].methodName).toBe('job');
311
+ });
312
+ });
313
+ describe('CronModule', () => {
314
+ const EVERY_MINUTE = '* * * * *';
315
+ describe('forRoot()', () => {
316
+ it('returns module configuration with defaults', () => {
317
+ const result = cron_module_1.CronModule.forRoot();
318
+ expect(result.module).toBe(cron_module_1.CronModule);
319
+ expect(result.providers).toContain(cron_service_1.CronService);
320
+ expect(result.exports).toContain(cron_service_1.CronService);
321
+ expect(result.global).toBe(true);
322
+ });
323
+ it('respects isGlobal: false', () => {
324
+ const result = cron_module_1.CronModule.forRoot({ isGlobal: false });
325
+ expect(result.global).toBe(false);
326
+ });
327
+ it('respects isGlobal: true explicitly', () => {
328
+ const result = cron_module_1.CronModule.forRoot({ isGlobal: true });
329
+ expect(result.global).toBe(true);
330
+ });
331
+ });
332
+ describe('registerJobsFromProvider()', () => {
333
+ it('registers cron jobs from a provider with @Cron decorated methods', () => {
334
+ const cronService = new cron_service_1.CronService();
335
+ core_1.Container.getInstance.mockReturnValue({
336
+ resolve: jest.fn().mockReturnValue(cronService),
337
+ });
338
+ class TaskService {
339
+ doTask() {
340
+ // noop
341
+ }
342
+ }
343
+ __decorate([
344
+ (0, cron_decorator_1.Cron)({ cronTime: EVERY_MINUTE, name: 'myTask' }),
345
+ __metadata("design:type", Function),
346
+ __metadata("design:paramtypes", []),
347
+ __metadata("design:returntype", void 0)
348
+ ], TaskService.prototype, "doTask", null);
349
+ const instance = new TaskService();
350
+ cron_module_1.CronModule.registerJobsFromProvider(instance);
351
+ expect(cronService.getJobCount()).toBeGreaterThan(0);
352
+ cronService.clearAll();
353
+ });
354
+ it('does nothing when CronService is not in container', () => {
355
+ core_1.Container.getInstance.mockReturnValue({
356
+ resolve: jest.fn().mockReturnValue(null),
357
+ });
358
+ class NoopService {
359
+ }
360
+ expect(() => cron_module_1.CronModule.registerJobsFromProvider(new NoopService())).not.toThrow();
361
+ });
362
+ it('does nothing when provider has no @Cron metadata', () => {
363
+ const cronService = new cron_service_1.CronService();
364
+ core_1.Container.getInstance.mockReturnValue({
365
+ resolve: jest.fn().mockReturnValue(cronService),
366
+ });
367
+ class EmptyService {
368
+ plainMethod() {
369
+ // noop
370
+ }
371
+ }
372
+ cron_module_1.CronModule.registerJobsFromProvider(new EmptyService());
373
+ expect(cronService.getJobCount()).toBe(0);
374
+ });
375
+ it('handles errors gracefully', () => {
376
+ core_1.Container.getInstance.mockImplementation(() => {
377
+ throw new Error('container not ready');
378
+ });
379
+ class TaskService {
380
+ }
381
+ expect(() => cron_module_1.CronModule.registerJobsFromProvider(new TaskService())).not.toThrow();
382
+ });
383
+ });
384
+ });
385
+ describe('CronExpression constants', () => {
386
+ it('EVERY_SECOND is a valid 6-field expression', () => {
387
+ expect(cron_types_1.CronExpression.EVERY_SECOND).toBe('* * * * * *');
388
+ });
389
+ it('EVERY_MINUTE is a valid expression', () => {
390
+ expect(cron_types_1.CronExpression.EVERY_MINUTE).toBe('0 * * * * *');
391
+ });
392
+ it('EVERY_HOUR is a valid expression', () => {
393
+ expect(cron_types_1.CronExpression.EVERY_HOUR).toBe('0 0 * * * *');
394
+ });
395
+ it('EVERY_DAY_AT_MIDNIGHT is a valid expression', () => {
396
+ expect(cron_types_1.CronExpression.EVERY_DAY_AT_MIDNIGHT).toBe('0 0 0 * * *');
397
+ });
398
+ it('EVERY_YEAR is a valid expression', () => {
399
+ expect(cron_types_1.CronExpression.EVERY_YEAR).toBe('0 0 0 1 1 *');
400
+ });
401
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazeljs/cron",
3
- "version": "0.2.0-beta.55",
3
+ "version": "0.2.0-beta.56",
4
4
  "description": "Cron job scheduling module for HazelJS framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,5 +50,5 @@
50
50
  "peerDependencies": {
51
51
  "@hazeljs/core": ">=0.2.0-beta.0"
52
52
  },
53
- "gitHead": "f2e54f346eea552595a44607999454a9e388cb9e"
53
+ "gitHead": "c2737e90974458a8438eee623726f0a453b66b8b"
54
54
  }