From 73be64a900f3b6488d4a970d6c99cbdeb79c8ea8 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Mon, 1 Apr 2019 12:56:31 +0800 Subject: [PATCH] Add packages/client/src/crud --- packages/client/src/crud/CRUD.test.tsx | 258 ++++++++++++++---- packages/client/src/crud/CRUDActions.ts | 6 +- packages/client/src/crud/CRUDReducer.ts | 15 +- packages/client/src/http/IHeader.ts | 2 +- .../client/src/test-utils/HTTPClientMock.ts | 9 +- 5 files changed, 221 insertions(+), 69 deletions(-) diff --git a/packages/client/src/crud/CRUD.test.tsx b/packages/client/src/crud/CRUD.test.tsx index dfe0794..d1525d4 100644 --- a/packages/client/src/crud/CRUD.test.tsx +++ b/packages/client/src/crud/CRUD.test.tsx @@ -1,7 +1,9 @@ import React from 'react' import {AnyAction} from 'redux' -import {CRUDActions, CRUDReducer} from './' +import {CRUDActions, CRUDReducer, ICRUDMethod} from './' import {HTTPClientMock, TestUtils, getError} from '../test-utils' +import {IMethod} from '@rondo/common' +import {IPendingAction} from '../actions' describe('CRUD', () => { @@ -27,7 +29,6 @@ describe('CRUD', () => { '/one/:oneId/two/:twoId': { get: { params: ITwoSpecificParams - body: ITwoCreateBody response: ITwo } put: { @@ -37,7 +38,7 @@ describe('CRUD', () => { } delete: { params: ITwoSpecificParams - response: {id: number} + response: {id: number} // TODO return ITwoSpecificParams } } '/one/:oneId/two': { @@ -60,7 +61,8 @@ describe('CRUD', () => { specificRoute: '/one/:oneId/two/:twoId', actionName: 'TEST', }) - const Crud = new CRUDReducer('TEST').reduce + const crudReducer = new CRUDReducer('TEST') + const Crud = crudReducer.reduce const test = new TestUtils() const reducer = test.combineReducers({ @@ -73,6 +75,8 @@ describe('CRUD', () => { return test.createStore({reducer})() } + type Store = ReturnType + afterEach(() => { http.mockClear() }) @@ -83,71 +87,211 @@ describe('CRUD', () => { }) }) - describe('GET_MANY', () => { + function expectActions(actionTypes: string[]) { + const state = store.getState() + // first action is redux initializer + expect(state.Logger.slice(1)).toEqual(actionTypes) + } - function getAction(store: ReturnType) { - const action = store.dispatch(actions.getMany({ - query: {}, + let store: Store + beforeEach(() => { + store = getStore() + }) + + function dispatch( + method: ICRUDMethod, + action: IPendingAction, + ) { + store.dispatch(action) + expect(store.getState().Crud.status[method].isLoading).toBe(true) + expectActions([action.type]) + return action + } + + function getUrl(method: ICRUDMethod) { + return method === 'post' || method === 'getMany' + ? '/one/1/two' + : '/one/1/two/2' + } + + function getHTTPMethod(method: ICRUDMethod): IMethod { + return method === 'getMany' ? 'get' : method + } + + describe('Promise rejections', () => { + const testCases: Array<{ + method: ICRUDMethod + params: any + }> = [{ + method: 'get', + params: { + params: {oneId: 1, twoId: 2}, + }, + }, { + method: 'getMany', + params: { params: {oneId: 1}, - })) - const state = store.getState() - expect(state.Crud.status.getMany.isLoading).toBe(true) - expect(state.Logger).toEqual([ - jasmine.any(String), - 'TEST_GET_MANY_PENDING', - ]) - return action - } + }, + }, { + method: 'post', + params: { + body: {name: 'test'}, + params: {oneId: 1, twoId: 2}, + }, + }, { + method: 'put', + params: { + body: {name: 'test'}, + params: {oneId: 1, twoId: 2}, + }, + }, { + method: 'delete', + params: { + body: {}, + params: {oneId: 1, twoId: 2}, + }, + }] - describe('TEST_GET_MANY_RESOLVED', () => { - beforeEach(() => { - http.mockAdd({ - method: 'get', - url: '/one/1/two', - params: {}, - }, [{id: 2, name: 'bla'}]) + testCases.forEach(testCase => { + + const {method} = testCase + describe(method, () => { + beforeEach(() => { + http.mockAdd({ + url: getUrl(method), + method: getHTTPMethod(method), + data: method === 'put' || method === 'post' || method === 'delete' + ? testCase.params.body + : undefined, + }, {error: 'Test Error'}, 400) + }) + + it(`updates status on error: ${method}`, async () => { + const action = actions[method](testCase.params) + dispatch(testCase.method, action) + await getError(action.payload) + const state = store.getState() + expect(state.Crud.byId).toEqual({}) + expect(state.Crud.ids).toEqual([]) + expect(state.Crud.status[method].isLoading).toBe(false) + // TODO use error from response + expect(state.Crud.status[method].error).toEqual('HTTP Status: 400') + expectActions([ + action.type, + action.type.replace(/_PENDING$/, '_REJECTED'), + ]) + }) }) - it('updates state', async () => { - const store = getStore() - const action = getAction(store) - await action.payload - const state = store.getState() - expect(state.Crud.status.getMany.isLoading).toBe(false) - expect(state.Crud.ids).toEqual([2]) - expect(state.Crud.byId[2]).toEqual({id: 2, name: 'bla'}) - expect(state.Logger).toEqual([ - jasmine.any(String), - 'TEST_GET_MANY_PENDING', - 'TEST_GET_MANY_RESOLVED', - ]) + }) + + }) + + describe('Resolved promises', () => { + const entity = {id: 100, name: 'test'} + + const testCases: Array<{ + method: ICRUDMethod + params: any + body?: any + response: any + }> = [{ + method: 'getMany', + params: {oneId: 1, twoId: 2}, + response: [entity], + }, { + method: 'get', + params: {oneId: 1, twoId: 2}, + response: entity, + }, { + method: 'post', + params: {oneId: 1}, + body: {name: entity.name}, + response: entity, + }, { + method: 'put', + params: {oneId: 1, twoId: 2}, + body: {name: entity.name}, + response: entity, + }, { + method: 'delete', + params: {oneId: 1, twoId: 2}, + response: {id: entity.id}, + }] + + testCases.forEach(testCase => { + const {method} = testCase + + describe(method, () => { + beforeEach(() => { + http.mockAdd({ + url: getUrl(method), + method: getHTTPMethod(method), + data: testCase.body, + }, testCase.response) + }) + + afterEach(() => { + http.mockClear() + }) + + it('updates state', async () => { + const action = dispatch(testCase.method, actions[method]({ + query: undefined, + params: testCase.params, + body: testCase.body, + })) + await action.payload + const state = store.getState() + expect(state.Crud.status.getMany.isLoading).toBe(false) + if (method === 'delete') { + expect(state.Crud.ids).toEqual([]) + expect(state.Crud.byId[entity.id]).toBe(undefined) + } else { + if (method !== 'put') { + expect(state.Crud.ids).toEqual([entity.id]) + } + expect(state.Crud.byId[entity.id]).toEqual(entity) + } + }) }) }) - describe('TEST_GET_MANY_REJECTED', () => { + describe('POST then DELETE', () => { + + const postTestCase = testCases.find(t => t.method === 'post')! + const deleteTestCase = testCases.find(t => t.method === 'delete')! + beforeEach(() => { http.mockAdd({ - method: 'get', - url: '/one/1/two', - params: {}, - }, {error: 'Test Error'}, 400) + url: getUrl(postTestCase.method), + method: getHTTPMethod(postTestCase.method), + data: postTestCase.body, + }, postTestCase.response) + http.mockAdd({ + url: getUrl(deleteTestCase.method), + method: getHTTPMethod(deleteTestCase.method), + data: deleteTestCase.body, + }, deleteTestCase.response) }) - it('updates state', async () => { - const store = getStore() - const action = getAction(store) - await getError(action.payload) - const state = store.getState() - expect(state.Crud.status.getMany.isLoading).toBe(false) - // TODO use error from response - expect(state.Crud.status.getMany.error).toBe('HTTP Status: 400') - expect(state.Crud.ids).toEqual([]) - expect(state.Crud.byId).toEqual({}) - expect(state.Logger).toEqual([ - jasmine.any(String), - 'TEST_GET_MANY_PENDING', - 'TEST_GET_MANY_REJECTED', - ]) + afterEach(() => { + http.mockClear() + }) + + it('removes id and entity from state', async () => { + const action1 = store.dispatch(actions.post({ + params: postTestCase.params, + body: postTestCase.body, + })) + await action1.payload + expect(store.getState().Crud.ids).toEqual([entity.id]) + const action2 = store.dispatch(actions.delete({ + params: deleteTestCase.params, + body: deleteTestCase.body, + })) + await action2.payload + expect(store.getState().Crud.ids).toEqual([]) }) }) diff --git a/packages/client/src/crud/CRUDActions.ts b/packages/client/src/crud/CRUDActions.ts index 3610f3d..20305a4 100644 --- a/packages/client/src/crud/CRUDActions.ts +++ b/packages/client/src/crud/CRUDActions.ts @@ -1,6 +1,8 @@ import {IRoutes} from '@rondo/common' import {IHTTPClient, ITypedRequestParams} from '../http' +export type Optional = T extends {} ? T : undefined + interface ICRUDActionTypes { readonly get: string readonly put: string @@ -66,7 +68,7 @@ export class CRUDActions< } get(params: { - query: T[GET]['get']['query'], + query: Optional, params: T[GET]['get']['params'], }) { return { @@ -106,7 +108,7 @@ export class CRUDActions< } getMany(params: { - query: T[GET_MANY]['get']['query'], + query: Optional, params: T[GET_MANY]['get']['params'], }) { return { diff --git a/packages/client/src/crud/CRUDReducer.ts b/packages/client/src/crud/CRUDReducer.ts index 1344ec5..a07a967 100644 --- a/packages/client/src/crud/CRUDReducer.ts +++ b/packages/client/src/crud/CRUDReducer.ts @@ -19,11 +19,11 @@ export interface ICRUDState { } export interface ICRUDStatus { - readonly post: ICRUDMethodStatus - readonly put: ICRUDMethodStatus - readonly delete: ICRUDMethodStatus - readonly get: ICRUDMethodStatus - readonly getMany: ICRUDMethodStatus + readonly post: ICRUDMethodStatus + readonly put: ICRUDMethodStatus + readonly delete: ICRUDMethodStatus + readonly get: ICRUDMethodStatus + readonly getMany: ICRUDMethodStatus } export interface ICRUDActions { @@ -76,7 +76,7 @@ export class CRUDReducer { }, } - this.actionTypes = this.getActionTypes(actionName) + this.actionTypes = this.getActionTypes() } protected getPromiseActionNames(type: string) { @@ -87,7 +87,8 @@ export class CRUDReducer { } } - protected getActionTypes(actionName: string) { + protected getActionTypes() { + const {actionName} = this return { put: this.getPromiseActionNames(actionName + '_PUT'), post: this.getPromiseActionNames(actionName + '_POST'), diff --git a/packages/client/src/http/IHeader.ts b/packages/client/src/http/IHeader.ts index 6c7cedc..1c35fe1 100644 --- a/packages/client/src/http/IHeader.ts +++ b/packages/client/src/http/IHeader.ts @@ -1,3 +1,3 @@ export interface IHeader { - [key: string]: string + readonly [key: string]: string } diff --git a/packages/client/src/test-utils/HTTPClientMock.ts b/packages/client/src/test-utils/HTTPClientMock.ts index 5a04182..1edf321 100644 --- a/packages/client/src/test-utils/HTTPClientMock.ts +++ b/packages/client/src/test-utils/HTTPClientMock.ts @@ -49,7 +49,7 @@ export class HTTPClientMock extends HTTPClient { if (!this.mocks.hasOwnProperty(key)) { setImmediate(() => { const err = new Error( - 'No mock for request: ' + key + '\nAvailable mocks:' + + 'No mock for request: ' + key + '\nAvailable mocks: ' + Object.keys(this.mocks)) reject(err) currentRequest.finished = true @@ -76,7 +76,12 @@ export class HTTPClientMock extends HTTPClient { } protected serialize(req: IRequest) { - return JSON.stringify(req, null, ' ') + return JSON.stringify({ + method: req.method, + url: req.url, + params: req.params, + data: req.data, + }, null, ' ') } /**