CommunitySolidServer/test/unit/util/GuardedStream.test.ts
Joachim Van Herwegen fd45779159 feat: Pipe streams with the pump library
The library handles some edge cases we didn't yet.
The GuardedStream was also updated to ignore error listeners
already attached to the stream (since pump adds internal listeners).
2021-02-10 16:00:35 +01:00

179 lines
6.8 KiB
TypeScript

import { Readable } from 'stream';
import type { Logger } from '../../../src/logging/Logger';
import { getLoggerFor } from '../../../src/logging/LogUtil';
import { guardStream, isGuarded } from '../../../src/util/GuardedStream';
import { readableToString } from '../../../src/util/StreamUtil';
jest.mock('../../../src/logging/LogUtil', (): any => {
const logger: Logger = { error: jest.fn() } as any;
return { getLoggerFor: (): Logger => logger };
});
const logger: jest.Mocked<Logger> = getLoggerFor('GuardedStream') as any;
jest.useFakeTimers();
describe('GuardedStream', (): void => {
beforeEach((): void => {
jest.clearAllMocks();
});
describe('#guardStream', (): void => {
it('has no effect if no error is thrown.', async(): Promise<void> => {
const stream = Readable.from([ 'data' ]);
expect(isGuarded(stream)).toBe(false);
const guarded = guardStream(stream);
expect(guarded).toBe(stream);
expect(isGuarded(stream)).toBe(true);
expect(isGuarded(guarded)).toBe(true);
await expect(readableToString(guarded)).resolves.toBe('data');
});
it('returns the stream if it is already guarded.', async(): Promise<void> => {
const stream = Readable.from([ 'data' ]);
expect(isGuarded(stream)).toBe(false);
const guarded = guardStream(stream);
expect(guarded).toBe(stream);
expect(isGuarded(stream)).toBe(true);
expect(isGuarded(guarded)).toBe(true);
expect(guardStream(guarded)).toBe(stream);
expect(isGuarded(stream)).toBe(true);
expect(isGuarded(guarded)).toBe(true);
expect(guarded.listenerCount('error')).toBe(1);
expect(guarded.listenerCount('newListener')).toBe(1);
expect(guarded.listenerCount('removeListener')).toBe(0);
await expect(readableToString(guarded)).resolves.toBe('data');
});
it('emits errors when listeners are currently attached.', async(): Promise<void> => {
const stream = guardStream(Readable.from([ 'data' ]));
const listener = jest.fn();
stream.on('error', listener);
const error = new Error('error');
stream.emit('error', error);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenLastCalledWith(error);
});
it('emits guarded errors when new listeners are attached.', async(): Promise<void> => {
const errors = [ new Error('0'), new Error('1') ];
const stream = guardStream(Readable.from([ 'data' ]));
stream.emit('error', errors[0]);
stream.emit('error', errors[1]);
const errorListeners = [ jest.fn(), jest.fn(), jest.fn() ];
stream.addListener('error', errorListeners[0]);
stream.addListener('error', errorListeners[1]);
stream.addListener('error', errorListeners[2]);
const endListener = jest.fn();
stream.addListener('end', endListener);
expect(errorListeners[0]).toHaveBeenCalledTimes(0);
expect(errorListeners[1]).toHaveBeenCalledTimes(0);
expect(errorListeners[2]).toHaveBeenCalledTimes(0);
expect(endListener).toHaveBeenCalledTimes(0);
await new Promise((resolve): any => setImmediate(resolve));
expect(errorListeners[0]).toHaveBeenCalledTimes(2);
expect(errorListeners[0]).toHaveBeenNthCalledWith(1, errors[0]);
expect(errorListeners[0]).toHaveBeenNthCalledWith(2, errors[1]);
expect(errorListeners[1]).toHaveBeenCalledTimes(2);
expect(errorListeners[1]).toHaveBeenNthCalledWith(1, errors[0]);
expect(errorListeners[1]).toHaveBeenNthCalledWith(2, errors[1]);
expect(errorListeners[2]).toHaveBeenCalledTimes(2);
expect(errorListeners[1]).toHaveBeenNthCalledWith(1, errors[0]);
expect(errorListeners[1]).toHaveBeenNthCalledWith(2, errors[1]);
expect(endListener).toHaveBeenCalledTimes(0);
});
it('ignores error listeners that were already attached.', async(): Promise<void> => {
const stream = Readable.from([ 'data' ]);
stream.addListener('error', jest.fn());
guardStream(stream);
stream.emit('error', new Error('error'));
jest.advanceTimersByTime(1000);
expect(logger.error).toHaveBeenCalledTimes(1);
});
it('ignores error listeners after calling guardStream a second time.', async(): Promise<void> => {
const stream = Readable.from([ 'data' ]);
guardStream(stream);
stream.addListener('error', jest.fn());
// This will cause the above error listener to be ignored for logging purposes
guardStream(stream);
stream.emit('error', new Error('error'));
jest.advanceTimersByTime(1000);
expect(logger.error).toHaveBeenCalledTimes(1);
});
it('still works if error listeners get removed and added again.', async(): Promise<void> => {
const stream = guardStream(Readable.from([ 'data' ]));
// Make sure no unneeded listeners stay attached
const errorCb = jest.fn();
const errorCb2 = jest.fn();
stream.on('error', errorCb);
stream.on('error', errorCb2);
expect(stream.listenerCount('error')).toBe(3);
expect(stream.listenerCount('newListener')).toBe(1);
stream.removeListener('error', errorCb2);
expect(stream.listenerCount('error')).toBe(2);
expect(stream.listenerCount('newListener')).toBe(1);
stream.removeListener('error', errorCb);
expect(stream.listenerCount('error')).toBe(1);
expect(stream.listenerCount('newListener')).toBe(1);
const error = new Error('error');
stream.emit('error', error);
const errorCb3 = jest.fn();
stream.on('error', errorCb3);
await new Promise((resolve): any => setImmediate(resolve));
expect(errorCb).toHaveBeenCalledTimes(0);
expect(errorCb2).toHaveBeenCalledTimes(0);
expect(errorCb3).toHaveBeenCalledTimes(1);
expect(errorCb3).toHaveBeenLastCalledWith(error);
});
it('logs an error if nobody listens to the error.', async(): Promise<void> => {
const errors = [ new Error('0'), new Error('1'), new Error('2') ];
const stream = guardStream(Readable.from([ 'data' ]));
stream.emit('error', errors[0]);
jest.advanceTimersByTime(100);
stream.emit('error', errors[1]);
stream.emit('error', errors[2]);
// Only the first error gets logged
jest.advanceTimersByTime(900);
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(
'No error listener was attached but error was thrown: 0', { error: errors[0] },
);
jest.advanceTimersByTime(1000);
expect(logger.error).toHaveBeenCalledTimes(1);
const errorCb = jest.fn();
stream.on('error', errorCb);
await new Promise((resolve): any => setImmediate(resolve));
expect(errorCb).toHaveBeenCalledTimes(3);
expect(errorCb).toHaveBeenNthCalledWith(1, errors[0]);
expect(errorCb).toHaveBeenNthCalledWith(2, errors[1]);
expect(errorCb).toHaveBeenNthCalledWith(3, errors[2]);
});
});
});