From a9ee9806a4b1275013687ded0e8d0d680ce6ef92 Mon Sep 17 00:00:00 2001 From: Christopher Allford Date: Thu, 17 Sep 2020 13:31:39 -0700 Subject: [PATCH] Implemented the factory & repository for simple products --- tests/e2e/api/old_src/models/index.ts | 2 - tests/e2e/api/old_src/models/product.ts | 15 ---- .../e2e/api/old_src/models/simple-product.ts | 12 --- .../framework/__tests__/async-factory.spec.ts | 47 +++++++++++ .../__tests__/model-repository.spec.ts | 17 ++-- ...repository-factory.ts => async-factory.ts} | 37 +++------ .../e2e/api/src/framework/model-repository.ts | 2 +- tests/e2e/api/src/http/axios/axios-client.ts | 10 +-- tests/e2e/api/src/http/http-client.ts | 10 +-- tests/e2e/api/src/models/index.ts | 1 + tests/e2e/api/src/models/model.ts | 10 +-- .../product/__tests__/simple-product.spec.ts | 68 ++++++++++++++++ .../src/models/product/abstract-product.ts | 9 +++ .../api/src/models/product/simple-product.ts | 80 +++++++++++++++++++ 14 files changed, 234 insertions(+), 86 deletions(-) delete mode 100644 tests/e2e/api/old_src/models/index.ts delete mode 100644 tests/e2e/api/old_src/models/product.ts delete mode 100644 tests/e2e/api/old_src/models/simple-product.ts create mode 100644 tests/e2e/api/src/framework/__tests__/async-factory.spec.ts rename tests/e2e/api/src/framework/{repository-factory.ts => async-factory.ts} (56%) create mode 100644 tests/e2e/api/src/models/index.ts create mode 100644 tests/e2e/api/src/models/product/__tests__/simple-product.spec.ts create mode 100644 tests/e2e/api/src/models/product/abstract-product.ts create mode 100644 tests/e2e/api/src/models/product/simple-product.ts diff --git a/tests/e2e/api/old_src/models/index.ts b/tests/e2e/api/old_src/models/index.ts deleted file mode 100644 index 57d084aad6f..00000000000 --- a/tests/e2e/api/old_src/models/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Product } from './product'; -export { SimpleProduct, registerSimpleProduct } from './simple-product'; diff --git a/tests/e2e/api/old_src/models/product.ts b/tests/e2e/api/old_src/models/product.ts deleted file mode 100644 index 303f3520d76..00000000000 --- a/tests/e2e/api/old_src/models/product.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Model } from './model'; -import { DeepPartial } from 'fishery'; - -/** - * The base class for all product types. - */ -export abstract class Product extends Model { - public readonly name: string = ''; - public readonly regularPrice: string = ''; - - protected constructor( partial: DeepPartial< Product > = {} ) { - super( partial ); - Object.assign( this, partial ); - } -} diff --git a/tests/e2e/api/old_src/models/simple-product.ts b/tests/e2e/api/old_src/models/simple-product.ts deleted file mode 100644 index 14f3d1e1105..00000000000 --- a/tests/e2e/api/old_src/models/simple-product.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DeepPartial } from 'fishery'; -import { Product } from './product'; - -/** - * The simple product class. - */ -export class SimpleProduct extends Product { - public constructor( partial: DeepPartial< SimpleProduct > = {} ) { - super( partial ); - Object.assign( this, partial ); - } -} diff --git a/tests/e2e/api/src/framework/__tests__/async-factory.spec.ts b/tests/e2e/api/src/framework/__tests__/async-factory.spec.ts new file mode 100644 index 00000000000..98b59c06836 --- /dev/null +++ b/tests/e2e/api/src/framework/__tests__/async-factory.spec.ts @@ -0,0 +1,47 @@ +import { Model } from '../../models/model'; +import { AsyncFactory } from '../async-factory'; + +class DummyModel extends Model { + public name: string = ''; + + public constructor( partial?: Partial< DummyModel > ) { + super(); + Object.assign( this, partial ); + } +} + +describe( 'AsyncFactory', () => { + let factory: AsyncFactory< DummyModel >; + + beforeEach( () => { + let sequence = 1; + + factory = new AsyncFactory< DummyModel >( + ( { params } ) => { + const model = new DummyModel(); + model.name = params.name ?? ''; + return model; + }, + ( model ) => { + return Promise.resolve( new DummyModel( { id: sequence++, name: model.name } ) ); + }, + ); + } ); + + it( 'should create', async () => { + const model = await factory.create( { name: 'test-name' } ); + + expect( model ).toHaveProperty( 'id', 1 ); + expect( model ).toHaveProperty( 'name', 'test-name' ); + } ); + + it( 'should create many', async () => { + const models = await factory.createList( 2, { name: 'test-name' } ); + + expect( models ).toHaveLength( 2 ); + expect( models[ 0 ] ).toHaveProperty( 'id', 1 ); + expect( models[ 0 ] ).toHaveProperty( 'name', 'test-name' ); + expect( models[ 1 ] ).toHaveProperty( 'id', 2 ); + expect( models[ 1 ] ).toHaveProperty( 'name', 'test-name' ); + } ); +} ); diff --git a/tests/e2e/api/src/framework/__tests__/model-repository.spec.ts b/tests/e2e/api/src/framework/__tests__/model-repository.spec.ts index 05e3dbfd7ff..1458837f88d 100644 --- a/tests/e2e/api/src/framework/__tests__/model-repository.spec.ts +++ b/tests/e2e/api/src/framework/__tests__/model-repository.spec.ts @@ -4,11 +4,6 @@ import Mock = jest.Mock; class DummyModel extends Model { public name: string = ''; - - public onCreated( data: any ): void { - super.onCreated( data ); - this.name = data.name; - } } describe( 'ModelRepository', () => { @@ -24,28 +19,28 @@ describe( 'ModelRepository', () => { } ); it( 'should create', async () => { - mockCallback.mockReturnValue( Promise.resolve( dummyModel ) ); + mockCallback.mockResolvedValue( dummyModel ); await repository.create( dummyModel ); expect( mockCallback ).toHaveBeenCalledWith( dummyModel ); } ); it( 'should read', async () => { - mockCallback.mockReturnValue( Promise.resolve( dummyModel ) ); + mockCallback.mockResolvedValue( dummyModel ); - await repository.read( { id: 'test' } ); - expect( mockCallback ).toHaveBeenCalledWith( { id: 'test' } ); + await repository.read( { id: 1 } ); + expect( mockCallback ).toHaveBeenCalledWith( { id: 1 } ); } ); it( 'should update', async () => { - mockCallback.mockReturnValue( Promise.resolve( dummyModel ) ); + mockCallback.mockResolvedValue( dummyModel ); await repository.update( dummyModel ); expect( mockCallback ).toHaveBeenCalledWith( dummyModel ); } ); it( 'should delete', async () => { - mockCallback.mockReturnValue( Promise.resolve( true ) ); + mockCallback.mockResolvedValue( true ); await repository.delete( dummyModel ); expect( mockCallback ).toHaveBeenCalledWith( dummyModel ); diff --git a/tests/e2e/api/src/framework/repository-factory.ts b/tests/e2e/api/src/framework/async-factory.ts similarity index 56% rename from tests/e2e/api/src/framework/repository-factory.ts rename to tests/e2e/api/src/framework/async-factory.ts index 299a82a7410..eca7869682b 100644 --- a/tests/e2e/api/src/framework/repository-factory.ts +++ b/tests/e2e/api/src/framework/async-factory.ts @@ -1,25 +1,18 @@ -import { DeepPartial, Factory as BaseFactory, BuildOptions } from 'fishery'; -import { Repository } from './repository'; +import { BuildOptions, DeepPartial, Factory } from 'fishery'; import { GeneratorFnOptions } from 'fishery/dist/types'; -import { Model } from '../models/model'; -/** - * A factory that can be used to create models using an adapter. - */ -export class RepositoryFactory< T extends Model, I = any > extends BaseFactory< T, I > { - private repository: Repository< T > | null = null; - - public constructor( generator: ( opts: GeneratorFnOptions< T, I > ) => T ) { - super( generator ); - } +export class AsyncFactory< T, I = any > extends Factory< T, I > { + private readonly creator: ( model: T ) => Promise< T >; /** - * Sets the repository that the factory should use when creating data. + * Creates a new factory instance. * - * @param {Repository|null} repository The repository to set. + * @param {Function} generator The factory's generator function. + * @param {Function} creator The factory's creation function. */ - public setRepository( repository: Repository< T > | null ): void { - this.repository = repository; + public constructor( generator: ( opts: GeneratorFnOptions< T, I > ) => T, creator: ( model: T ) => Promise< T > ) { + super( generator ); + this.creator = creator; } /** @@ -30,12 +23,8 @@ export class RepositoryFactory< T extends Model, I = any > extends BaseFactory< * @return {Promise} Resolves to the created model. */ public create( params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T > { - if ( ! this.repository ) { - throw new Error( 'The factory has no repository to create using.' ); - } - const model = this.build( params, options ); - return this.repository.create( model ); + return this.creator( model ); } /** @@ -47,14 +36,10 @@ export class RepositoryFactory< T extends Model, I = any > extends BaseFactory< * @return {Promise} Resolves to the created models. */ public createList( number: number, params?: DeepPartial< T >, options?: BuildOptions< T, I > ): Promise< T[] > { - if ( ! this.repository ) { - throw new Error( 'The factory has no repository to create using.' ); - } - const models = this.buildList( number, params, options ); const promises: Promise< T >[] = []; for ( const model of models ) { - promises.push( this.repository.create( model ) ); + promises.push( this.create( model ) ); } return Promise.all( promises ); diff --git a/tests/e2e/api/src/framework/model-repository.ts b/tests/e2e/api/src/framework/model-repository.ts index d78a9dc1211..5d0f81c803d 100644 --- a/tests/e2e/api/src/framework/model-repository.ts +++ b/tests/e2e/api/src/framework/model-repository.ts @@ -9,7 +9,7 @@ type DeleteFn< T > = ( model: T ) => Promise< boolean >; * The standard parameters for reading a model. */ interface DefaultReadParams { - id: string; + id: number; } /** diff --git a/tests/e2e/api/src/http/axios/axios-client.ts b/tests/e2e/api/src/http/axios/axios-client.ts index 3bbb998ea81..b22e7f0afad 100644 --- a/tests/e2e/api/src/http/axios/axios-client.ts +++ b/tests/e2e/api/src/http/axios/axios-client.ts @@ -30,7 +30,7 @@ export class AxiosClient implements HTTPClient { * @param {*} params Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - public get< T >( + public get< T = any >( path: string, params?: any, ): Promise< HTTPResponse< T >> { @@ -44,7 +44,7 @@ export class AxiosClient implements HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - public post< T >( + public post< T = any >( path: string, data?: any, ): Promise< HTTPResponse< T >> { @@ -58,7 +58,7 @@ export class AxiosClient implements HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - public put< T >( + public put< T = any >( path: string, data?: any, ): Promise< HTTPResponse< T >> { @@ -72,7 +72,7 @@ export class AxiosClient implements HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - public patch< T >( + public patch< T = any >( path: string, data?: any, ): Promise< HTTPResponse< T >> { @@ -86,7 +86,7 @@ export class AxiosClient implements HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - public delete< T >( + public delete< T = any >( path: string, data?: any, ): Promise< HTTPResponse< T >> { diff --git a/tests/e2e/api/src/http/http-client.ts b/tests/e2e/api/src/http/http-client.ts index c0c3af6cf4a..bd736813ae3 100644 --- a/tests/e2e/api/src/http/http-client.ts +++ b/tests/e2e/api/src/http/http-client.ts @@ -31,7 +31,7 @@ export interface HTTPClient { * @param {*} params Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - get< T >( path: string, params?: any ): Promise< HTTPResponse< T > >; + get< T = any >( path: string, params?: any ): Promise< HTTPResponse< T > >; /** * Performs a POST request. @@ -40,7 +40,7 @@ export interface HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - post< T >( path: string, data?: any ): Promise< HTTPResponse< T > >; + post< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >; /** * Performs a PUT request. @@ -49,7 +49,7 @@ export interface HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - put< T >( path: string, data?: any ): Promise< HTTPResponse< T > >; + put< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >; /** * Performs a PATCH request. @@ -58,7 +58,7 @@ export interface HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - patch< T >( path: string, data?: any ): Promise< HTTPResponse< T > >; + patch< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >; /** * Performs a DELETE request. @@ -67,5 +67,5 @@ export interface HTTPClient { * @param {*} data Any parameters that should be passed in the request. * @return {Promise} Resolves to an HTTPResponse. */ - delete< T >( path: string, data?: any ): Promise< HTTPResponse< T > >; + delete< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >; } diff --git a/tests/e2e/api/src/models/index.ts b/tests/e2e/api/src/models/index.ts new file mode 100644 index 00000000000..a544c69253f --- /dev/null +++ b/tests/e2e/api/src/models/index.ts @@ -0,0 +1 @@ +export { SimpleProduct } from './product/simple-product'; diff --git a/tests/e2e/api/src/models/model.ts b/tests/e2e/api/src/models/model.ts index 0321e12f7e3..60a42802386 100644 --- a/tests/e2e/api/src/models/model.ts +++ b/tests/e2e/api/src/models/model.ts @@ -2,13 +2,5 @@ * A base class for all models. */ export abstract class Model { - private _id: number | null = null; - - public get id(): number | null { - return this._id; - } - - public onCreated( data: any ): void { - this._id = data.id; - } + public readonly id: number | null = null; } diff --git a/tests/e2e/api/src/models/product/__tests__/simple-product.spec.ts b/tests/e2e/api/src/models/product/__tests__/simple-product.spec.ts new file mode 100644 index 00000000000..597e5e3ef63 --- /dev/null +++ b/tests/e2e/api/src/models/product/__tests__/simple-product.spec.ts @@ -0,0 +1,68 @@ +import { SimpleProduct } from '../simple-product'; +import { mock, MockProxy } from 'jest-mock-extended'; +import { HTTPClient, HTTPResponse } from '../../../http'; +import { ModelRepository } from '../../../framework/model-repository'; + +describe( 'SimpleProduct', () => { + describe( 'restRepository', () => { + let httpClient: MockProxy< HTTPClient >; + let repository: ModelRepository< SimpleProduct >; + + beforeEach( () => { + httpClient = mock< HTTPClient >(); + repository = SimpleProduct.restRepository( httpClient ); + } ); + + it( 'should create', async () => { + httpClient.post.mockResolvedValue( new HTTPResponse( 200, {}, { id: 2 } ) ); + + const created = await repository.create( new SimpleProduct( { name: 'test' } ) ); + + expect( created ).toHaveProperty( 'id', 2 ); + expect( httpClient.post ).toHaveBeenCalledWith( + '/wc/v3/products', + { + name: 'test', + }, + ); + } ); + + it( 'should read', async () => { + httpClient.get.mockResolvedValue( + new HTTPResponse( 200, {}, { + id: 12, + name: 'test-name', + } ), + ); + + const read = await repository.read( { id: 12 } ); + + expect( read ).toHaveProperty( 'id', 12 ); + expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/products/12' ); + } ); + + it( 'should update', async () => { + httpClient.put.mockResolvedValue( new HTTPResponse( 200, {}, { id: 1 } ) ); + + const updated = await repository.update( new SimpleProduct( { id: 1, name: 'test' } ) ); + + expect( updated ).toHaveProperty( 'id', 1 ); + expect( httpClient.put ).toHaveBeenCalledWith( + '/wc/v3/products/1', + { + id: 1, + name: 'test', + }, + ); + } ); + + it( 'should delete', async () => { + httpClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) ); + + const response = await repository.delete( new SimpleProduct( { id: 123 } ) ); + + expect( response ).toBeTruthy(); + expect( httpClient.delete ).toHaveBeenCalledWith( '/wc/v3/products/123' ); + } ); + } ); +} ); diff --git a/tests/e2e/api/src/models/product/abstract-product.ts b/tests/e2e/api/src/models/product/abstract-product.ts new file mode 100644 index 00000000000..593191a5b85 --- /dev/null +++ b/tests/e2e/api/src/models/product/abstract-product.ts @@ -0,0 +1,9 @@ +import { Model } from '../model'; + +/** + * The base class for all product types. + */ +export abstract class AbstractProduct extends Model { + public readonly name: string = ''; + public readonly regularPrice: string = ''; +} diff --git a/tests/e2e/api/src/models/product/simple-product.ts b/tests/e2e/api/src/models/product/simple-product.ts new file mode 100644 index 00000000000..415de30388e --- /dev/null +++ b/tests/e2e/api/src/models/product/simple-product.ts @@ -0,0 +1,80 @@ +import { AbstractProduct } from './abstract-product'; +import * as faker from 'faker/locale/en'; +import { HTTPClient } from '../../http'; +import { ModelRepository } from '../../framework/model-repository'; +import { AsyncFactory } from '../../framework/async-factory'; + +/** + * The simple product class. + */ +export class SimpleProduct extends AbstractProduct { + public constructor( partial: Partial< SimpleProduct > = {} ) { + super(); + Object.assign( this, partial ); + } + + /** + * Creates a model repository configured for communicating via the REST API. + * + * @param {HTTPClient} httpClient The client for communicating via HTTP. + * @return {ModelRepository} The created repository. + */ + public static restRepository( httpClient: HTTPClient ): ModelRepository< SimpleProduct > { + return new ModelRepository( + async ( model ) => { + const response = await httpClient.post( + '/wc/v3/products', + { + name: model.name, + }, + ); + + return Promise.resolve( new SimpleProduct( { + id: response.data.id, + name: response.data.name, + } ) ); + }, + async ( params ) => { + const response = await httpClient.get( '/wc/v3/products/' + params.id ); + + const model = new SimpleProduct( + { + id: response.data.id, + name: response.data.name, + }, + ); + return Promise.resolve( model ); + }, + async ( model ) => { + return httpClient.put( + '/wc/v3/products/' + model.id, + { + id: model.id, + name: model.name, + }, + ).then( () => model ); + }, + async ( model ) => { + return httpClient.delete( '/wc/v3/products/' + model.id ).then( () => true ); + }, + ); + } + + /** + * Creates a new factory instance. + * + * @param {ModelRepository} repository The repository to use for creation. + * @return {AsyncFactory} The new factory instance. + */ + public static factory( repository: ModelRepository< SimpleProduct > ): AsyncFactory< SimpleProduct > { + return new AsyncFactory< SimpleProduct >( + ( { params } ) => { + return new SimpleProduct( { + name: params.name ?? faker.commerce.productName(), + regularPrice: params.regularPrice ?? faker.commerce.price(), + } ); + }, + repository.create, + ); + } +}