Skip to content

Commit d92d932

Browse files
committed
feat: add more tests to improve coverage
1 parent ccc2b4f commit d92d932

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*!
2+
* IMQClient console logger fallback branches coverage
3+
*/
4+
import './mocks';
5+
import { expect } from 'chai';
6+
import * as sinon from 'sinon';
7+
import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..';
8+
9+
class ConsoleClient extends IMQClient {
10+
@remote()
11+
public async ok(name?: string, meta?: IMQMetadata, delay?: IMQDelay) {
12+
return this.remoteCall<string>(...arguments);
13+
}
14+
@remote()
15+
public async boom() {
16+
return this.remoteCall<any>(...arguments);
17+
}
18+
}
19+
20+
describe('IMQClient console logger fallbacks', () => {
21+
let client: ConsoleClient;
22+
23+
afterEach(async () => {
24+
try { await client?.destroy(); } catch { /* ignore */ }
25+
sinon.restore();
26+
});
27+
28+
it('should use console logger when BEFORE hook fails', async () => {
29+
const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
30+
client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); } });
31+
await client.start();
32+
33+
const imq: any = (client as any).imq;
34+
sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => {
35+
const id = 'C1';
36+
setImmediate(() => imq.emit('message', { to: id, request, data: 'good' }));
37+
return id;
38+
});
39+
40+
const res = await client.ok('x');
41+
expect(res).to.equal('good');
42+
expect(warn.called).to.equal(true);
43+
expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR);
44+
});
45+
46+
it('should use console logger when AFTER hook fails (resolve and reject paths)', async () => {
47+
const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
48+
client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); } });
49+
await client.start();
50+
51+
const imq: any = (client as any).imq;
52+
const send = sinon.stub(imq, 'send');
53+
54+
// success path
55+
send.onFirstCall().callsFake(async (_to: string, request: any) => {
56+
const id = 'C2';
57+
setImmediate(() => imq.emit('message', { to: id, request, data: 'S' }));
58+
return id;
59+
});
60+
61+
// reject path
62+
send.onSecondCall().callsFake(async (_to: string, request: any) => {
63+
const id = 'C3';
64+
setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') }));
65+
return id;
66+
});
67+
68+
const ok = await client.ok('ok');
69+
expect(ok).to.equal('S');
70+
71+
try { await client.boom(); } catch { /* expected */ }
72+
73+
expect(warn.callCount).to.be.greaterThan(0);
74+
const messages = warn.getCalls().map(c => String(c.args[0])).join(' ');
75+
expect(messages).to.contain(AFTER_HOOK_ERROR);
76+
});
77+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*!
2+
* IMQService logger fallback coverage test
3+
*/
4+
import { expect } from 'chai';
5+
import * as sinon from 'sinon';
6+
import { uuid } from '@imqueue/core';
7+
import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..';
8+
9+
class ConsoleLoggerService extends IMQService {
10+
@expose()
11+
public ping() { return 'pong'; }
12+
}
13+
14+
describe('IMQService handleRequest logger fallback to console', () => {
15+
let warnStub: sinon.SinonSpy;
16+
17+
beforeEach(() => {
18+
warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {});
19+
});
20+
21+
afterEach(async () => {
22+
sinon.restore();
23+
});
24+
25+
it('should use console when no custom logger provided and catch BEFORE hook error', async () => {
26+
const beforeCall = async () => { throw new Error('before fails'); };
27+
const service: any = new ConsoleLoggerService({ beforeCall }); // no logger provided
28+
29+
const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] };
30+
const id = uuid();
31+
32+
await service.start();
33+
34+
// Spy send to ensure regular flow continues and send is called even without afterCall
35+
const sendSpy = sinon.spy(service.imq, 'send');
36+
37+
service.imq.emit('message', request, id);
38+
39+
await new Promise((resolve, reject) => setTimeout(() => {
40+
try {
41+
expect(warnStub.called).to.equal(true);
42+
const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR);
43+
expect(hasBefore).to.equal(true);
44+
expect(sendSpy.called).to.equal(true);
45+
resolve(undefined);
46+
} catch (e) { reject(e); }
47+
}, 1));
48+
49+
await service.destroy();
50+
});
51+
52+
it('should use console when afterCall throws (send() logger fallback)', async () => {
53+
const afterCall = async () => { throw new Error('after fails'); };
54+
const service: any = new ConsoleLoggerService({ afterCall }); // no logger provided
55+
56+
const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] };
57+
const id = uuid();
58+
59+
await service.start();
60+
61+
service.imq.emit('message', request, id);
62+
63+
await new Promise((resolve, reject) => setTimeout(() => {
64+
try {
65+
expect(warnStub.called).to.equal(true);
66+
const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR);
67+
expect(hasAfter).to.equal(true);
68+
resolve(undefined);
69+
} catch (e) { reject(e); }
70+
}, 1));
71+
72+
await service.destroy();
73+
});
74+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*!
2+
* IMQService signal handler and publish() coverage tests
3+
*/
4+
import { expect } from 'chai';
5+
import * as sinon from 'sinon';
6+
import { uuid } from '@imqueue/core';
7+
import { IMQService, IMQRPCRequest, expose } from '..';
8+
9+
class SignalTestService extends IMQService {
10+
@expose()
11+
public ping() {
12+
return 'pong';
13+
}
14+
}
15+
16+
describe('IMQService signal handler and publish()', () => {
17+
afterEach(async () => {
18+
sinon.restore();
19+
});
20+
21+
it('should call destroy() and log error on process signal (without exiting)', async () => {
22+
const onStub = sinon.stub(process as any, 'on');
23+
const handlers: { sig: string; fn: (...args: any[]) => any }[] = [];
24+
onStub.callsFake((sig: string, fn: any) => { handlers.push({ sig, fn }); return process as any; });
25+
26+
// use fake timers to avoid triggering real process.exit from setTimeout
27+
const clock = sinon.useFakeTimers();
28+
29+
const logger: any = { info: () => {}, warn: () => {}, error: sinon.spy() };
30+
const service: any = new SignalTestService({ logger });
31+
32+
// make destroy reject to hit catch(logger.error)
33+
sinon.stub(service, 'destroy').callsFake(async () => { throw new Error('boom'); });
34+
35+
// simulate first registered signal handler
36+
expect(handlers.length).to.be.greaterThan(0);
37+
await handlers[0].fn();
38+
39+
// let microtasks settle
40+
await Promise.resolve();
41+
42+
expect(logger.error.called).to.equal(true);
43+
44+
clock.restore();
45+
});
46+
47+
it('publish() should delegate to imq.publish', async () => {
48+
const logger: any = { info: () => {}, warn: () => {}, error: () => {} };
49+
const service: any = new SignalTestService({ logger });
50+
const stub = sinon.stub(service.imq, 'publish').resolves(undefined as any);
51+
52+
await service.publish({ id: uuid() } as any);
53+
54+
expect(stub.calledOnce).to.equal(true);
55+
});
56+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* RedisCache error branches: methods before init should throw
3+
*/
4+
import { expect } from 'chai';
5+
import { RedisCache, REDIS_CLIENT_INIT_ERROR } from '../..';
6+
7+
describe('cache/RedisCache errors when not initialized', () => {
8+
beforeEach(() => { delete (RedisCache as any).redis; });
9+
10+
it('get() should throw if redis not initialized', async () => {
11+
const cache = new RedisCache();
12+
let threw = false;
13+
try {
14+
await cache.get('x');
15+
} catch (e: any) {
16+
threw = true;
17+
expect(e).to.be.instanceOf(TypeError);
18+
expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
19+
}
20+
expect(threw).to.equal(true);
21+
});
22+
23+
it('set() should throw if redis not initialized', async () => {
24+
const cache = new RedisCache();
25+
let threw = false;
26+
try {
27+
await cache.set('x', 1);
28+
} catch (e: any) {
29+
threw = true;
30+
expect(e).to.be.instanceOf(TypeError);
31+
expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
32+
}
33+
expect(threw).to.equal(true);
34+
});
35+
36+
it('del() should throw if redis not initialized', async () => {
37+
const cache = new RedisCache();
38+
let threw = false;
39+
try {
40+
await cache.del('x');
41+
} catch (e: any) {
42+
threw = true;
43+
expect(e).to.be.instanceOf(TypeError);
44+
expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
45+
}
46+
expect(threw).to.equal(true);
47+
});
48+
49+
it('purge() should throw if redis not initialized', async () => {
50+
const cache = new RedisCache();
51+
let threw = false;
52+
try {
53+
await cache.purge('mask');
54+
} catch (e: any) {
55+
threw = true;
56+
expect(e).to.be.instanceOf(TypeError);
57+
expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
58+
}
59+
expect(threw).to.equal(true);
60+
});
61+
});

0 commit comments

Comments
 (0)