Skip to content

Commit

Permalink
fix(test): middleware, post, auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Petr Juna committed Nov 7, 2019
1 parent 86cbee1 commit 25dfd7a
Show file tree
Hide file tree
Showing 26 changed files with 417 additions and 24,583 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"rules": {
"@typescript-eslint/no-namespace": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-parameter-properties": [0]
"@typescript-eslint/no-parameter-properties": [0],
"@typescript-eslint/no-empty-interface": 0
}
}
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_js:
script:
- npm run lint
- npm run prettier:lint
- npm run test:e2e
- npm run build

jobs:
Expand Down
1 change: 1 addition & 0 deletions ava.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
files: ['./e2e/**/*.e2e-spec.ts'],
compileEnhancements: false,
extensions: ['ts'],
require: ['ts-node/register', './src/env.ts'],
Expand Down
27 changes: 27 additions & 0 deletions e2e/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { store } from '../src/test.pre';
import test from 'ava';
import { requestSignInAction, resetAuthAction } from '../src/auth/actions';
import { AuthSignInModel, AuthSuccessModel } from '@pyxismedia/lib-model';

test.serial.afterEach(() => {
store.dispatch(resetAuthAction());
});

test.serial.cb('should authentificate user', t => {
store.dispatch(requestSignInAction(new AuthSignInModel({ email: 'karel@vomacka.cz', password: '12345' })));
const unsubscribe = store.subscribe(() => {
const state = store.getState();
t.log(state.auth);
// Following props are unique so we don't know exact values
t.truthy(state.auth.id);
t.truthy(state.auth.createdAt);
t.truthy(state.auth.token);
// We know exact value
t.is(state.auth.userId, AuthSuccessModel.MOCK.userId);
// To kame sure that schema is not not providing redundant property
// @ts-ignore
t.falsy(state.auth._v);
t.end();
unsubscribe();
});
});
158 changes: 158 additions & 0 deletions e2e/post.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
RequestPostsModel,
PostModel,
CreatePostModel,
PostStateEnum,
AuthSignInModel,
DeletePostModel,
} from '@pyxismedia/lib-model';
import postsEnJson from '@pyxismedia/lib-model/build/post/post.en-mock.json';
import test from 'ava';
import {
requestPostsAction,
resetPostAction,
createPostAction,
PostActions,
deletePostAction,
} from '../src/post/actions';
import { requestSignInAction, AuthActions, resetAuthAction } from '../src/auth/actions';
import { switchMap } from 'rxjs/operators';
import { resetToastAction, ToastActions } from '../src/toast/actions';
import { configureStore } from '../src/store';
import { Store } from 'redux';
import { Exception } from '../src/exception';

let store: Store;

test.serial.beforeEach(() => {
store = configureStore();
});

test.serial.afterEach(() => {
store.dispatch(resetPostAction());
store.dispatch(resetAuthAction());
store.dispatch(resetToastAction());
});

test.serial.cb('should deliver posts without skipping', t => {
store.dispatch(requestPostsAction(new RequestPostsModel(0)));
const unsubscribe = store.subscribe(() => {
t.deepEqual(store.getState().post.posts, {
collection: (postsEnJson as unknown) as PostModel[],
skip: 0,
});
unsubscribe();
t.end();
});
});

test.serial.cb('should deliver posts and skip by 4', t => {
const skip = 4;
store.dispatch(requestPostsAction(new RequestPostsModel(skip)));
const unsubscribe = store.subscribe(() => {
const state = store.getState();
const collection = (postsEnJson.slice(4) as unknown) as PostModel[];
const expected = {
collection,
skip,
};
t.deepEqual(state.post.posts, expected);
unsubscribe();
t.end();
});
});

test.serial.cb('should deliver posts and skip by 3', t => {
const skip = 3;
store.dispatch(requestPostsAction(new RequestPostsModel(skip)));
const unsubscribe = store.subscribe(() => {
const state = store.getState();
const collection = (postsEnJson.slice(skip) as unknown) as PostModel[];
const expected = {
collection,
skip,
};
t.deepEqual(state.post.posts, expected);
unsubscribe();
t.end();
});
});

test.serial.cb('should respond with error for unaunthentificated user', t => {
const post = new CreatePostModel(
'Test 1',
'Subtitle Test 1',
'Lorem ispum',
'#',
PostStateEnum.DRAFT,
['label 1'],
// TODO: Does Author exists? If not it should raise issue
'5d9e2c4974146800ea816336',
// TODO: Does Author exists? If not it should raise issue
'5d9e2c4974146800ea816336',
);
store.dispatch(createPostAction(post));
const unsubscribe = store.subscribe(() => {
const state = store.getState();
if (state.toast) {
t.deepEqual(state.action.type, 'CREATE_TOAST');
t.deepEqual(state.toast, { messages: [new Exception('Unauthorized', 401)] });
t.end();
} else {
t.fail('Toast in store does not exist.');
}
unsubscribe();
});
});

test.serial.cb('should create post and deliver it', t => {
const post = new CreatePostModel(
'Test 2',
'Subtitle Test 2',
'Lorem ispum',
'#',
PostStateEnum.DRAFT,
['label 1'],
// TODO: Does Author exists? If not it should raise issue
'5d9e2c4974146800ea816336',
// TODO: Does Author exists? If not it should raise issue
'5d9e2c4974146800ea816336',
);
const signIn = new AuthSignInModel({ email: 'karel@vomacka.cz', password: '12345' });
//@ts-ignore
store
.dispatch(requestSignInAction(signIn))
//@ts-ignore
.asActionObservable(AuthActions.DELIVER_SIGNIN)
.pipe(
//@ts-ignore
switchMap(() => {
//@ts-ignore
return store.dispatch(createPostAction(post)).asActionObservable(PostActions.DELIVER_POST);
}),
)
.subscribe(() => {
const state = store.getState();
// t.log('STATE STATE STATE', state);
const expected = { ...post };
if (state.post.post) {
t.is(state.post.post.title, expected.title);
t.is(state.post.post.subtitle, expected.subtitle);
t.is(state.post.post.content, expected.content);
t.is(state.post.post.image, expected.image);
t.is(state.post.post.state, expected.state);
t.deepEqual(state.post.post.labels, expected.labels);
t.is(state.post.post.subtitle, expected.subtitle);
t.is(state.post.post.createdBy, expected.createdBy);
t.is(state.post.post.section, expected.section);
console.log('id in test', state.post.post);
store
.dispatch(deletePostAction(new DeletePostModel(state.post.post.id)))
//@ts-ignore
.asActionObservable(ToastActions.CREATE_TOAST)
.subscribe(() => {
t.end();
});
}
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"commit": "commit",
"lint": "eslint -c .eslintrc.json './src/**/*.ts'",
"release": "semantic-release",
"pretest:watch": "npm run mongoimport",
"test": "ava --verbose",
"test:watch": "ava --watch --verbose",
"prettier": "prettier --config .prettierrc --write './src/**/*.ts'",
Expand Down
2 changes: 2 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ActionType } from 'typesafe-actions';
import * as postActions from './post/actions';
import * as toastActions from './toast/actions';
import * as authActions from './auth/actions';

const actions = {
...postActions,
...toastActions,
...authActions,
};

export type ActionTypes = ActionType<typeof actions>;
18 changes: 18 additions & 0 deletions src/auth/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AuthSignInModel, AuthSuccessModel } from '@pyxismedia/lib-model';
import { createAction } from 'typesafe-actions';

export enum AuthActions {
REQUEST_SIGNIN = 'REQUEST_SIGNIN',
DELIVER_SIGNIN = 'DELIVER_SIGNIN',
RESET_SIGNIN = 'RESET_SIGNIN',
}

export const requestSignInAction = createAction(AuthActions.REQUEST_SIGNIN, action => (payload: AuthSignInModel) =>
action(payload),
);

export const deliverSignInAction = createAction(AuthActions.DELIVER_SIGNIN, action => (payload: AuthSuccessModel) =>
action(payload),
);

export const resetAuthAction = createAction(AuthActions.RESET_SIGNIN);
25 changes: 25 additions & 0 deletions src/auth/epics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RootEpic } from '../epics';
import { filter, switchMap, map, catchError } from 'rxjs/operators';
import { isActionOf, PayloadAction } from 'typesafe-actions';
import { requestSignInAction, AuthActions, deliverSignInAction } from './actions';
import { AuthSignInModel } from '../../../lib-model/src/auth/auth-signin.model';
import { API_AUTH } from '../constants';
import { AuthSuccessModel } from '../../../lib-model/src/auth/auth-success.model';
import { createToast } from '../toast/actions';
import { of } from 'rxjs';

export const requestSignInEpic: RootEpic = (action$, _$, { crud }) =>
action$.pipe(
filter(isActionOf(requestSignInAction)),
switchMap(
({ payload }: PayloadAction<AuthActions.REQUEST_SIGNIN, AuthSignInModel>): Promise<AuthSuccessModel> => {
return crud.post(API_AUTH, JSON.stringify(payload));
},
),
map((auth: AuthSuccessModel) => {
return deliverSignInAction(auth);
}),
catchError(error => {
return of(createToast(error));
}),
);
25 changes: 25 additions & 0 deletions src/auth/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as authActions from './actions';
import { getType, ActionType } from 'typesafe-actions';
import { AuthSuccessModel } from '../../../lib-model/src/auth/auth-success.model';

export type AuthActionTypes = ActionType<typeof authActions>;

export interface AuthState extends AuthSuccessModel {}

export const initialState = {
id: '',
userId: '',
token: '',
};

export function authReducer(state: AuthState = initialState, action: AuthActionTypes): AuthState {
switch (action.type) {
case getType(authActions.deliverSignInAction):
return { ...state, ...action.payload };
case getType(authActions.resetAuthAction):
return { ...initialState };
case getType(authActions.requestSignInAction):
default:
return { ...state };
}
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const NODE_ENV = process.env.NODE_ENV || 'production';
export const API_HOSTNAME = process.env.API_HOSTNAME || 'http://srv-nest.pyxis.media';
// API Endpoints
export const API_POST = `${API_HOSTNAME}/post`;
export const API_AUTH = `${API_HOSTNAME}/auth`;
30 changes: 23 additions & 7 deletions src/crud.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Exception } from './exception';
const { keys } = Object;

class REST {
Expand All @@ -15,6 +16,7 @@ interface Params {
enum CRUD {
GET = 'GET',
POST = 'POST',
DELETE = 'DELETE',
}

export class Crud {
Expand All @@ -26,9 +28,19 @@ export class Crud {
this.rest.append('Content-Type', 'application/json');
}

public async fetch<T>(url: RequestInfo, init: RequestInit): Promise<T> {
const result = await fetch(url, { ...init, headers: this.rest.headers });
return result.json();
public async fetch<T>(url: RequestInfo, init: RequestInit, token?: string): Promise<T> {
const headers = { ...this.rest.headers };
if (token) {
// @ts-ignore
headers['Authorization'] = `Bearer ${token}`;
}
const requestInit = { ...init, headers };
const result = await fetch(url, requestInit);
const response = await result.json();
if (!result.ok) {
throw new Exception(response.error, response.statusCode);
}
return response;
}

private static getParamsString(params: Params): string {
Expand All @@ -37,15 +49,19 @@ export class Crud {
.join('&');
}

public get<T>(input: string, params?: Params): Promise<T> {
public get<T>(input: string, params?: Params, token?: string): Promise<T> {
let query;
if (params) {
query = Crud.getParamsString(params);
}
return this.fetch(`${input}${query}`, { method: CRUD.GET });
return this.fetch(`${input}${query ? '?' : ''}${query}`, { method: CRUD.GET }, token);
}

public post<T>(url: string, body: BodyInit, token?: string): Promise<T> {
return this.fetch(url, { method: CRUD.POST, body }, token);
}

public post<T>(url: string, body: BodyInit): Promise<T> {
return this.fetch(url, { method: CRUD.POST, body });
public delete<T>(url: string, token?: string): Promise<T> {
return this.fetch(url, { method: CRUD.DELETE }, token);
}
}
4 changes: 0 additions & 4 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { execSync } from 'child_process';

require('dotenv-flow').config({
// eslint-disable-next-line @typescript-eslint/camelcase
node_env: process.env.NODE_ENV === 'test' || !process.env.NODE_ENV ? 'development' : process.env.NODE_ENV,
Expand All @@ -8,5 +6,3 @@ require('dotenv-flow').config({
console.log(`INFO: NODE_ENV=${process.env.NODE_ENV} \n`);
console.log(`INFO: API_HOSTNAME=${process.env.API_HOSTNAME} \n`);
console.warn('WARNING: Make sure you have correct environment to make test working!');

execSync('npm run mongoimport');
12 changes: 10 additions & 2 deletions src/epics.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { combineEpics, Epic } from 'redux-observable';
import { requestPostsEpic, createPostEpic } from './post/epics';
import { requestPostsEpic, createPostEpic, deletePostEpic } from './post/epics';
import { ActionTypes } from './actions';
import { RootState } from './reducers';
import { Dependencies } from './middlewares';
import { requestSignInEpic } from './auth/epics';
import { createToastEpic } from './toast/epics';

export type RootEpic = Epic<ActionTypes, ActionTypes, RootState, Dependencies>;

export const rootEpic = combineEpics(requestPostsEpic, createPostEpic);
export const rootEpic = combineEpics(
requestPostsEpic,
createPostEpic,
deletePostEpic,
requestSignInEpic,
createToastEpic,
);
Loading

0 comments on commit 25dfd7a

Please sign in to comment.