Add tests for the provider & the entities

This commit is contained in:
Convly 2022-10-25 17:58:00 +02:00
parent 0937aef112
commit bb75a2433d
3 changed files with 321 additions and 0 deletions

View File

@ -0,0 +1,152 @@
import type { IEntity } from '../../../../types';
import { Readable, PassThrough } from 'stream';
import {
collect,
getStrapiFactory,
createMockedReadableFactory,
getContentTypes,
} from './test-utils';
import { createEntitiesStream, createEntitiesTransformStream } from '../entities';
describe('Local Strapi Source Provider - Entities Streaming', () => {
describe('Create Entities Stream', () => {
test('Should return an empty stream if there is no content type', async () => {
const customContentTypes = {};
const stream = createMockedReadableFactory<never>({});
const strapi = getStrapiFactory({
contentTypes: customContentTypes,
entityService: { stream },
})();
const entitiesStream = createEntitiesStream(strapi);
// The returned value should be a Readable stream instance
expect(entitiesStream).toBeInstanceOf(Readable);
const entities = await collect<IEntity<never>>(entitiesStream);
// The stream should not have been called since there is no content types
// Note: This check must happen AFTER we've collected the results
expect(stream).not.toHaveBeenCalled();
// We have 0 * 0 entities
expect(entities).toHaveLength(0);
});
test('Should return a stream with 4 entities from 2 content types', async () => {
const stream = createMockedReadableFactory({
foo: [
{ id: 1, title: 'First title' },
{ id: 2, title: 'Second title' },
],
bar: [
{ id: 1, age: 42 },
{ id: 2, age: 84 },
],
});
const strapi = getStrapiFactory({
contentTypes: getContentTypes(),
entityService: { stream },
})();
const entitiesStream = createEntitiesStream(strapi);
// The returned value should be a Readable stream instance
expect(entitiesStream).toBeInstanceOf(Readable);
const results = await collect(entitiesStream);
// Should have been called with 'foo', then 'bar'
// Note: This check must happen AFTER we've collected the results
expect(stream).toHaveBeenCalledTimes(2);
// We have 2 * 2 entities
expect(results).toHaveLength(4);
const matchContentTypeUIDs = new RegExp(`(${Object.keys(getContentTypes()).join('|')})`);
// Each result should contain the entity and its parent content type
results.forEach((result) => {
expect(result).toMatchObject(
expect.objectContaining({
entity: expect.objectContaining({
id: expect.any(Number),
}),
contentType: expect.objectContaining({
uid: expect.stringMatching(matchContentTypeUIDs),
attributes: expect.any(Object),
}),
})
);
});
});
});
describe('Create Entities Transform Stream', () => {
test('Should transform entities to the Strapi Data Transfer Format', async () => {
const entities = [
{
entity: { id: 1, title: 'My first foo' },
contentType: { uid: 'foo' },
},
{
entity: { id: 4, title: 'Another Foo' },
contentType: { uid: 'foo' },
},
{
entity: { id: 12, title: 'Last foo' },
contentType: { uid: 'foo' },
},
{
entity: { id: 1, age: 21 },
contentType: { uid: 'bar' },
},
{
entity: { id: 2, age: 42 },
contentType: { uid: 'bar' },
},
{
entity: { id: 7, age: 84 },
contentType: { uid: 'bar' },
},
{
entity: { id: 9, age: 0 },
contentType: { uid: 'bar' },
},
];
const matchContentTypeUIDs = new RegExp(
`(${Object.values(entities)
.map((entity) => entity.contentType.uid)
.join('|')})`
);
const entitiesStream = Readable.from(entities);
const transformStream = createEntitiesTransformStream();
expect(transformStream).toBeInstanceOf(PassThrough);
// Connect the data source to the transformation stream
const pipeline = entitiesStream.pipe(transformStream);
const transformedEntities = await collect(pipeline);
// Check the amount of transformed entities matches the initial amount
expect(transformedEntities).toHaveLength(entities.length);
// Each result should contain a type (uid), and id and some data
transformedEntities.forEach((entity) => {
expect(entity).toMatchObject(
expect.objectContaining({
type: expect.stringMatching(matchContentTypeUIDs),
id: expect.any(Number),
data: expect.any(Object),
})
);
});
});
});
});

View File

@ -0,0 +1,112 @@
import type { IEntity } from '../../../../types';
import { Readable } from 'stream';
import { collect, getStrapiFactory } from './test-utils';
import { createLocalStrapiSourceProvider } from '../';
describe('Local Strapi Source Provider', () => {
describe('Boostrap', () => {
test('Should not have a defined Strapi instance if bootstrap has not been called', () => {
const provider = createLocalStrapiSourceProvider({ getStrapi: getStrapiFactory() });
expect(provider.strapi).not.toBeDefined();
});
test('Should have a defined Strapi instance if bootstrap has been called', async () => {
const provider = createLocalStrapiSourceProvider({ getStrapi: getStrapiFactory() });
await provider.bootstrap();
expect(provider.strapi).toBeDefined();
});
});
describe('Close', () => {
test('Should destroy the strapi instance if autoDestroy is undefined ', async () => {
const destroy = jest.fn();
const provider = createLocalStrapiSourceProvider({
getStrapi: getStrapiFactory({ destroy }),
});
await provider.bootstrap();
await provider.close();
expect(destroy).toHaveBeenCalled();
});
test('Should destroy the strapi instance if autoDestroy is true ', async () => {
const destroy = jest.fn();
const provider = createLocalStrapiSourceProvider({
getStrapi: getStrapiFactory({ destroy }),
autoDestroy: true,
});
await provider.bootstrap();
await provider.close();
expect(destroy).toHaveBeenCalled();
});
});
describe('Streaming Entities', () => {
test('Should throw an error if strapi is not defined', async () => {
const provider = createLocalStrapiSourceProvider({ getStrapi: getStrapiFactory() });
await expect(() => provider.streamEntities()).rejects.toThrowError(
'Not able to stream entities. Strapi instance not found'
);
});
test('Should successfully create a readable stream with all available entities', async () => {
const contentTypes = {
foo: { uid: 'foo', attributes: { title: { type: 'string' } } },
bar: { uid: 'bar', attributes: { age: { type: 'number' } } },
};
const stream = jest.fn((uid: string, _query: unknown) => {
if (uid === 'foo') {
return Readable.from([
{ id: 1, title: 'First title' },
{ id: 2, title: 'Second title' },
]);
}
if (uid === 'bar') {
return Readable.from([
{ id: 1, age: 42 },
{ id: 2, age: 84 },
]);
}
});
const provider = createLocalStrapiSourceProvider({
getStrapi: getStrapiFactory({
contentTypes,
entityService: { stream },
}),
});
await provider.bootstrap();
const entitiesStream = (await provider.streamEntities()) as Readable;
const entities = await collect<IEntity<'foo' | 'bar'>>(entitiesStream);
// Should have been called with 'foo', then 'bar'
expect(stream).toHaveBeenCalledTimes(2);
// The returned value should be a Readable stream instance
expect(entitiesStream).toBeInstanceOf(Readable);
// We have 2 * 2 entities
expect(entities).toHaveLength(4);
// Each entity should follow the transfer format
entities.forEach((entity) => {
expect(entity).toMatchObject({
type: expect.any(String),
id: expect.any(Number),
data: expect.any(Object),
});
});
});
});
});

View File

@ -0,0 +1,57 @@
import { Readable } from 'stream';
/**
* Collect every entity in a Readable stream
*/
export const collect = <T = unknown>(stream: Readable): Promise<T[]> => {
const chunks: T[] = [];
return new Promise((resolve) => {
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('end', () => {
stream.destroy();
resolve(chunks);
});
});
};
/**
* Create a "Strapi" like object factory based on the
* given params and cast it to the correct type
*/
export const getStrapiFactory =
<
T extends {
[key in keyof Partial<Strapi.Strapi>]: unknown;
}
>(
properties?: T
) =>
() => {
return { ...properties } as Strapi.Strapi;
};
/**
* Union type used to represent the default content types available
*/
export type ContentType = 'foo' | 'bar';
/**
* Factory to get default content types test values
*/
export const getContentTypes = (): {
[key in ContentType]: { uid: key; attributes: { [attribute: string]: unknown } };
} => ({
foo: { uid: 'foo', attributes: { title: { type: 'string' } } },
bar: { uid: 'bar', attributes: { age: { type: 'number' } } },
});
/**
* Create a factory of readable streams (wrapped with a jest mock function)
*/
export const createMockedReadableFactory = <T extends string = ContentType>(source: {
[ct in T]: Array<{ id: number; [key: string]: unknown }>;
}) =>
jest.fn((uid: T) => {
return Readable.from(source[uid] || []);
});