import { Logger } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { NEVER, Observable, throwError } from 'rxjs'; import { isBrokerConnected, publishConfirmed } from './broker.util'; /** * `ClientProxy.emit()` returns a cold Observable that, for RMQ, completes without * emitting once `dispatchEvent` settles — and rejects if the publish fails. These * fakes reproduce each of those three shapes. */ function clientEmitting(source: Observable): ClientProxy { return { emit: jest.fn().mockReturnValue(source) } as unknown as ClientProxy; } describe('publishConfirmed', () => { const logger = { error: jest.fn() } as unknown as Logger; beforeEach(() => jest.clearAllMocks()); it('is true when the publish completes (broker confirmed)', async () => { // Completes with no value — the success shape, and the case that throws // EmptyError without a defaultIfEmpty. const client = clientEmitting(new Observable((s) => s.complete())); await expect(publishConfirmed(client, 'send-sms', {}, logger)).resolves.toBe(true); }); it('is false when the publish never settles, rather than hanging', async () => { // A broker that is down: amqp-connection-manager buffers the publish and the // promise would never resolve. The timeout is what stops one dead broker from // hanging every caller of sendSms/sendEmail. const client = clientEmitting(NEVER); await expect(publishConfirmed(client, 'send-sms', {}, logger, 20)).resolves.toBe( false, ); expect(logger.error).toHaveBeenCalled(); }); it('is false when the publish errors', async () => { const client = clientEmitting(throwError(() => new Error('channel closed'))); await expect(publishConfirmed(client, 'send-email', {}, logger)).resolves.toBe( false, ); expect(logger.error).toHaveBeenCalled(); }); }); describe('isBrokerConnected', () => { /** Stands in for `ClientProxy.unwrap()`, which returns the AmqpConnectionManager. */ function clientUnwrapping(manager: unknown): ClientProxy { return { unwrap: () => manager } as unknown as ClientProxy; } it('reports the connection manager state', () => { expect(isBrokerConnected(clientUnwrapping({ isConnected: () => true }))).toBe( true, ); expect(isBrokerConnected(clientUnwrapping({ isConnected: () => false }))).toBe( false, ); }); it('is false when unwrap throws — the client never connected', () => { // ClientRMQ.unwrap() throws "Not initialized" while its internal client is // null, which is what a failed boot-time connect leaves behind. That is a // real down signal and must not be softened to "unknown". const uninitialised = { unwrap: () => { throw new Error('Not initialized. Please call the "connect" method first.'); }, } as unknown as ClientProxy; expect(isBrokerConnected(uninitialised)).toBe(false); }); it('is null — not a guess — when the manager lacks isConnected or it throws', () => { // Guards the health endpoint against reporting "ok" if amqp-connection-manager // or Nest changes shape and the accessor we rely on disappears. expect(isBrokerConnected(clientUnwrapping(null))).toBeNull(); expect(isBrokerConnected(clientUnwrapping({}))).toBeNull(); expect( isBrokerConnected( clientUnwrapping({ isConnected: () => { throw new Error('boom'); }, }), ), ).toBeNull(); }); });