Added tests/refactored users module

This commit is contained in:
Philipp Czora
2018-07-12 16:02:37 +02:00
parent 84ae2248b2
commit d8ac7281e3
5 changed files with 290 additions and 31 deletions

View File

@@ -4,13 +4,14 @@ import DeleteUserButton from "./DeleteUserButton";
import type { User } from "../types/User";
type Props = {
user: User,
entry: { loading: boolean, error: Error, user: User },
deleteUser: string => void
};
export default class UserRow extends React.Component<Props> {
render() {
const { user, deleteUser } = this.props;
const { deleteUser } = this.props;
const user = this.props.entry.entry;
return (
<tr>
<td>{user.name}</td>

View File

@@ -4,13 +4,14 @@ import UserRow from "./UserRow";
import type { User } from "../types/User";
type Props = {
users: Array<User>,
entries: [{ loading: boolean, error: Error, user: User }],
deleteUser: string => void
};
class UserTable extends React.Component<Props> {
render() {
const { users, deleteUser } = this.props;
const { deleteUser } = this.props;
const entries = this.props.entries;
return (
<table>
<thead>
@@ -22,8 +23,10 @@ class UserTable extends React.Component<Props> {
</tr>
</thead>
<tbody>
{users.map((user, index) => {
return <UserRow key={index} user={user} deleteUser={deleteUser} />;
{entries.map((entry, index) => {
return (
<UserRow key={index} entry={entry} deleteUser={deleteUser} />
);
})}
</tbody>
</table>

View File

@@ -2,15 +2,27 @@
import React from "react";
import { connect } from "react-redux";
import { fetchUsers, addUser, editUser, deleteUser } from "../modules/users";
import {
fetchUsers,
addUser,
editUser,
deleteUser,
getUsersFromState
} from "../modules/users";
import UserForm from "./UserForm";
import UserTable from "./UserTable";
import type { User } from "../types/User";
type UserEntry = {
loading: boolean,
error: Error,
user: User
};
type Props = {
login: boolean,
error: Error,
users: Array<User>,
userEntries: Array<UserEntry>,
fetchUsers: () => void,
deleteUser: string => void,
addUser: User => void,
@@ -31,7 +43,7 @@ class Users extends React.Component<Props> {
};
render() {
const { users, deleteUser } = this.props;
const { userEntries, deleteUser } = this.props;
const testUser: User = {
name: "user",
displayName: "user_display",
@@ -40,13 +52,13 @@ class Users extends React.Component<Props> {
active: true,
admin: true
};
if (users) {
if (userEntries) {
return (
<section className="section">
<div className="container">
<h1 className="title">SCM</h1>
<h2 className="subtitle">Users</h2>
<UserTable users={users} deleteUser={deleteUser} />
<UserTable entries={userEntries} deleteUser={deleteUser} />
{/* <UserForm submitForm={this.submitForm} /> */}
<UserForm submitForm={user => {}} user={testUser} />
</div>
@@ -59,8 +71,12 @@ class Users extends React.Component<Props> {
}
const mapStateToProps = state => {
const userEntries = getUsersFromState(state);
if (!userEntries) {
return {};
}
return {
users: state.users.users
userEntries
};
};

View File

@@ -3,22 +3,22 @@ import { apiClient, PAGE_NOT_FOUND_ERROR } from "../../apiclient";
import type { User } from "../types/User";
import { ThunkDispatch } from "redux-thunk";
const FETCH_USERS = "scm/users/FETCH";
const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS";
const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE";
const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND";
export const FETCH_USERS = "scm/users/FETCH";
export const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS";
export const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE";
export const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND";
const ADD_USER = "scm/users/ADD";
const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS";
const ADD_USER_FAILURE = "scm/users/ADD_FAILURE";
export const ADD_USER = "scm/users/ADD";
export const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS";
export const ADD_USER_FAILURE = "scm/users/ADD_FAILURE";
const EDIT_USER = "scm/users/EDIT";
const EDIT_USER_SUCCESS = "scm/users/EDIT_SUCCESS";
const EDIT_USER_FAILURE = "scm/users/EDIT_FAILURE";
export const EDIT_USER = "scm/users/EDIT";
export const EDIT_USER_SUCCESS = "scm/users/EDIT_SUCCESS";
export const EDIT_USER_FAILURE = "scm/users/EDIT_FAILURE";
const DELETE_USER = "scm/users/DELETE";
const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS";
const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE";
export const DELETE_USER = "scm/users/DELETE";
export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS";
export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE";
const USERS_URL = "users";
@@ -45,7 +45,7 @@ function usersNotFound(url: string) {
}
export function fetchUsers() {
return function(dispatch: ThunkDispatch) {
return function(dispatch: any) {
dispatch(requestUsers());
return apiClient
.get(USERS_URL)
@@ -179,21 +179,58 @@ export function deleteUser(link: string) {
};
}
export function getUsersFromState(state) {
if (!state.users.users) {
return null;
}
const userNames = state.users.users.entries;
if (!userNames) {
return null;
}
var userEntries = new Array();
for (let userName of userNames) {
userEntries.push(state.users.usersByNames[userName]);
}
return userEntries;
}
function extractUsersByNames(users: Array<User>, userNames: Array<string>) {
var usersByNames = {};
for (let user of users) {
usersByNames[user.name] = {
entry: user
};
}
return usersByNames;
}
export default function reducer(state: any = {}, action: any = {}) {
switch (action.type) {
case FETCH_USERS:
return {
loading: true,
error: null
};
case DELETE_USER:
return {
...state,
users: null,
loading: true
users: null
};
case FETCH_USERS_SUCCESS:
const users = action.payload._embedded.users;
const userNames = users.map(user => user.name);
const usersByNames = {...state.usersByNames, extractUsersByNames(users, userNames)};
return {
...state,
users: {
error: null,
users: action.payload._embedded.users,
entries: userNames,
loading: false
},
usersByNames
};
case FETCH_USERS_FAILURE:
case DELETE_USER_FAILURE:

View File

@@ -0,0 +1,202 @@
//@flow
import React from "react";
import { configure, shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import {
FETCH_USERS,
FETCH_USERS_SUCCESS,
fetchUsers,
FETCH_USERS_FAILURE
} from "./users";
import reducer from "./users";
import "raf/polyfill";
configure({ adapter: new Adapter() });
const userZaphod = {
active: true,
admin: true,
creationDate: "2018-07-11T12:23:49.027Z",
displayName: "Z. Beeblebrox",
mail: "president@heartofgold.universe",
name: "zaphod",
password: "__dummypassword__",
type: "xml",
properties: {},
_links: {
self: {
href: "http://localhost:8081/scm/api/rest/v2/users/zaphod"
},
delete: {
href: "http://localhost:8081/scm/api/rest/v2/users/zaphod"
},
update: {
href: "http://localhost:8081/scm/api/rest/v2/users/zaphod"
}
}
};
const userFord = {
active: true,
admin: false,
creationDate: "2018-07-06T13:21:18.459Z",
displayName: "F. Prefect",
mail: "ford@prefect.universe",
name: "ford",
password: "__dummypassword__",
type: "xml",
properties: {},
_links: {
self: {
href: "http://localhost:8081/scm/api/rest/v2/users/ford"
},
delete: {
href: "http://localhost:8081/scm/api/rest/v2/users/ford"
},
update: {
href: "http://localhost:8081/scm/api/rest/v2/users/ford"
}
}
};
const responseBodyZaphod = {
page: 0,
pageTotal: 1,
_links: {
self: {
href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10"
},
first: {
href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10"
},
last: {
href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10"
},
create: {
href: "http://localhost:3000/scm/api/rest/v2/users/"
}
},
_embedded: {
users: [userZaphod]
}
};
const responseBody = {
page: 0,
pageTotal: 1,
_links: {
self: {
href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10"
},
first: {
href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10"
},
last: {
href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10"
},
create: {
href: "http://localhost:3000/scm/api/rest/v2/users/"
}
},
_embedded: {
users: [userZaphod, userFord]
}
};
const response = {
headers: { "content-type": "application/json" },
responseBody
};
describe("fetch tests", () => {
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
test("successful users fetch", () => {
fetchMock.getOnce("/scm/api/rest/v2/users", response);
const expectedActions = [
{ type: FETCH_USERS },
{
type: FETCH_USERS_SUCCESS,
payload: response
}
];
const store = mockStore({});
return store.dispatch(fetchUsers()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
test("me fetch failed", () => {
fetchMock.getOnce("/scm/api/rest/v2/users", {
status: 500
});
const store = mockStore({});
return store.dispatch(fetchUsers()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USERS);
expect(actions[1].type).toEqual(FETCH_USERS_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
});
describe("reducer tests", () => {
test("users request", () => {
var newState = reducer({}, { type: FETCH_USERS });
expect(newState.loading).toBeTruthy();
expect(newState.error).toBeNull();
});
test("fetch users successful", () => {
var newState = reducer(
{},
{ type: FETCH_USERS_SUCCESS, payload: responseBody }
);
expect(newState.users).toEqual({
entries: ["zaphod", "ford"],
error: null,
loading: false
});
expect(newState.usersByNames).toEqual({
zaphod: {
entry: userZaphod
},
ford: {
entry: userFord
}
});
});
test("reducer does not replace whole usersByNames map", () => {
const oldState = {
usersByNames: {
ford: {
entry: userFord
}
}
};
const newState = reducer(oldState, {
type: FETCH_USERS_SUCCESS,
payload: responseBodyZaphod
});
expect(newState.usersByNames["zaphod"]).toBeDefined();
expect(newState.usersByNames["ford"]).toBeDefined();
});
});