mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
Added tests/refactored users module
This commit is contained in:
@@ -4,13 +4,14 @@ import DeleteUserButton from "./DeleteUserButton";
|
|||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User,
|
entry: { loading: boolean, error: Error, user: User },
|
||||||
deleteUser: string => void
|
deleteUser: string => void
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class UserRow extends React.Component<Props> {
|
export default class UserRow extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { user, deleteUser } = this.props;
|
const { deleteUser } = this.props;
|
||||||
|
const user = this.props.entry.entry;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{user.name}</td>
|
<td>{user.name}</td>
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import UserRow from "./UserRow";
|
|||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
users: Array<User>,
|
entries: [{ loading: boolean, error: Error, user: User }],
|
||||||
deleteUser: string => void
|
deleteUser: string => void
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserTable extends React.Component<Props> {
|
class UserTable extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { users, deleteUser } = this.props;
|
const { deleteUser } = this.props;
|
||||||
|
const entries = this.props.entries;
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -22,8 +23,10 @@ class UserTable extends React.Component<Props> {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{users.map((user, index) => {
|
{entries.map((entry, index) => {
|
||||||
return <UserRow key={index} user={user} deleteUser={deleteUser} />;
|
return (
|
||||||
|
<UserRow key={index} entry={entry} deleteUser={deleteUser} />
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -2,15 +2,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
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 UserForm from "./UserForm";
|
||||||
import UserTable from "./UserTable";
|
import UserTable from "./UserTable";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
|
|
||||||
|
type UserEntry = {
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
|
user: User
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
login: boolean,
|
login: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
users: Array<User>,
|
userEntries: Array<UserEntry>,
|
||||||
fetchUsers: () => void,
|
fetchUsers: () => void,
|
||||||
deleteUser: string => void,
|
deleteUser: string => void,
|
||||||
addUser: User => void,
|
addUser: User => void,
|
||||||
@@ -31,7 +43,7 @@ class Users extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { users, deleteUser } = this.props;
|
const { userEntries, deleteUser } = this.props;
|
||||||
const testUser: User = {
|
const testUser: User = {
|
||||||
name: "user",
|
name: "user",
|
||||||
displayName: "user_display",
|
displayName: "user_display",
|
||||||
@@ -40,13 +52,13 @@ class Users extends React.Component<Props> {
|
|||||||
active: true,
|
active: true,
|
||||||
admin: true
|
admin: true
|
||||||
};
|
};
|
||||||
if (users) {
|
if (userEntries) {
|
||||||
return (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h1 className="title">SCM</h1>
|
<h1 className="title">SCM</h1>
|
||||||
<h2 className="subtitle">Users</h2>
|
<h2 className="subtitle">Users</h2>
|
||||||
<UserTable users={users} deleteUser={deleteUser} />
|
<UserTable entries={userEntries} deleteUser={deleteUser} />
|
||||||
{/* <UserForm submitForm={this.submitForm} /> */}
|
{/* <UserForm submitForm={this.submitForm} /> */}
|
||||||
<UserForm submitForm={user => {}} user={testUser} />
|
<UserForm submitForm={user => {}} user={testUser} />
|
||||||
</div>
|
</div>
|
||||||
@@ -59,8 +71,12 @@ class Users extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
const userEntries = getUsersFromState(state);
|
||||||
|
if (!userEntries) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
users: state.users.users
|
userEntries
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ import { apiClient, PAGE_NOT_FOUND_ERROR } from "../../apiclient";
|
|||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import { ThunkDispatch } from "redux-thunk";
|
import { ThunkDispatch } from "redux-thunk";
|
||||||
|
|
||||||
const FETCH_USERS = "scm/users/FETCH";
|
export const FETCH_USERS = "scm/users/FETCH";
|
||||||
const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS";
|
export const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS";
|
||||||
const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE";
|
export const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE";
|
||||||
const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND";
|
export const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND";
|
||||||
|
|
||||||
const ADD_USER = "scm/users/ADD";
|
export const ADD_USER = "scm/users/ADD";
|
||||||
const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS";
|
export const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS";
|
||||||
const ADD_USER_FAILURE = "scm/users/ADD_FAILURE";
|
export const ADD_USER_FAILURE = "scm/users/ADD_FAILURE";
|
||||||
|
|
||||||
const EDIT_USER = "scm/users/EDIT";
|
export const EDIT_USER = "scm/users/EDIT";
|
||||||
const EDIT_USER_SUCCESS = "scm/users/EDIT_SUCCESS";
|
export const EDIT_USER_SUCCESS = "scm/users/EDIT_SUCCESS";
|
||||||
const EDIT_USER_FAILURE = "scm/users/EDIT_FAILURE";
|
export const EDIT_USER_FAILURE = "scm/users/EDIT_FAILURE";
|
||||||
|
|
||||||
const DELETE_USER = "scm/users/DELETE";
|
export const DELETE_USER = "scm/users/DELETE";
|
||||||
const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS";
|
export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS";
|
||||||
const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE";
|
export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE";
|
||||||
|
|
||||||
const USERS_URL = "users";
|
const USERS_URL = "users";
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ function usersNotFound(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fetchUsers() {
|
export function fetchUsers() {
|
||||||
return function(dispatch: ThunkDispatch) {
|
return function(dispatch: any) {
|
||||||
dispatch(requestUsers());
|
dispatch(requestUsers());
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(USERS_URL)
|
.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 = {}) {
|
export default function reducer(state: any = {}, action: any = {}) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_USERS:
|
case FETCH_USERS:
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
case DELETE_USER:
|
case DELETE_USER:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
users: null,
|
users: null
|
||||||
loading: true
|
|
||||||
};
|
};
|
||||||
case FETCH_USERS_SUCCESS:
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: null,
|
users: {
|
||||||
users: action.payload._embedded.users,
|
error: null,
|
||||||
loading: false
|
entries: userNames,
|
||||||
|
loading: false
|
||||||
|
},
|
||||||
|
usersByNames
|
||||||
};
|
};
|
||||||
case FETCH_USERS_FAILURE:
|
case FETCH_USERS_FAILURE:
|
||||||
case DELETE_USER_FAILURE:
|
case DELETE_USER_FAILURE:
|
||||||
|
|||||||
202
scm-ui/src/users/modules/users.test.js
Normal file
202
scm-ui/src/users/modules/users.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user