Files
edr-platform/apps/edr-freight-api/src/modules/notifications/broker.util.spec.ts

91 lines
3.4 KiB
TypeScript

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<unknown>): 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<never>((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();
});
});