mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 15:05:44 +01:00
Merged branches
This commit is contained in:
@@ -28,5 +28,25 @@
|
||||
},
|
||||
"user-form": {
|
||||
"submit": "Submit"
|
||||
},
|
||||
"add-user": {
|
||||
"title": "Create User",
|
||||
"subtitle": "Create a new user"
|
||||
},
|
||||
"single-user": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown user error",
|
||||
"navigation-label": "Navigation",
|
||||
"actions-label": "Actions",
|
||||
"information-label": "Information",
|
||||
"back-label": "Back"
|
||||
},
|
||||
"validation": {
|
||||
"mail-invalid": "This email is invalid",
|
||||
"name-invalid": "This name is invalid",
|
||||
"displayname-invalid": "This displayname is invalid",
|
||||
"password-invalid": "Password has to be at least six characters",
|
||||
"passwordValidation-invalid": "Passwords have to be the same",
|
||||
"validatePassword": "Please validate password here"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
@@ -7,7 +8,9 @@ type Props = {
|
||||
value?: string,
|
||||
type?: string,
|
||||
autofocus?: boolean,
|
||||
onChange: string => void
|
||||
onChange: string => void,
|
||||
validationError: boolean,
|
||||
errorMessage: string
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
@@ -37,8 +40,9 @@ class InputField extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { type, placeholder, value } = this.props;
|
||||
|
||||
const { type, placeholder, value, validationError, errorMessage } = this.props;
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
@@ -47,13 +51,17 @@ class InputField extends React.Component<Props> {
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
}}
|
||||
className="input"
|
||||
className={ classNames(
|
||||
"input",
|
||||
errorView
|
||||
)}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={this.handleInput}
|
||||
/>
|
||||
</div>
|
||||
{helper}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { User } from "../types/User";
|
||||
import { InputField, Checkbox } from "../../components/forms";
|
||||
import { SubmitButton } from "../../components/buttons";
|
||||
import {translate} from "react-i18next";
|
||||
import type {User} from "../types/User";
|
||||
import {Checkbox, InputField} from "../../components/forms";
|
||||
import {SubmitButton} from "../../components/buttons";
|
||||
|
||||
type Props = {
|
||||
submitForm: User => void,
|
||||
@@ -11,10 +11,23 @@ type Props = {
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class UserForm extends React.Component<Props, User> {
|
||||
type State = {
|
||||
user: User,
|
||||
mailValidationError: boolean,
|
||||
nameValidationError: boolean,
|
||||
displayNameValidationError: boolean,
|
||||
passwordValidationError: boolean,
|
||||
validatePasswordError: boolean,
|
||||
validatePassword: string
|
||||
};
|
||||
|
||||
|
||||
class UserForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
user: {
|
||||
name: "",
|
||||
displayName: "",
|
||||
mail: "",
|
||||
@@ -22,22 +35,30 @@ class UserForm extends React.Component<Props, User> {
|
||||
admin: false,
|
||||
active: false,
|
||||
_links: {}
|
||||
},
|
||||
mailValidationError: false,
|
||||
displayNameValidationError: false,
|
||||
nameValidationError: false,
|
||||
passwordValidationError: false,
|
||||
validatePasswordError: false,
|
||||
validatePassword: ""
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ ...this.props.user });
|
||||
this.setState({ user: {...this.props.user} });
|
||||
}
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
this.props.submitForm(this.state);
|
||||
this.props.submitForm(this.state.user);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const user = this.state;
|
||||
|
||||
const user = this.state.user;
|
||||
const ButtonClickable = (this.state.validatePasswordError || this.state.nameValidationError || this.state.mailValidationError || this.state.validatePasswordError
|
||||
|| this.state.displayNameValidationError || user.name === undefined|| user.displayName === undefined);
|
||||
let nameField = null;
|
||||
if (!this.props.user) {
|
||||
nameField = (
|
||||
@@ -45,6 +66,8 @@ class UserForm extends React.Component<Props, User> {
|
||||
label={t("user.name")}
|
||||
onChange={this.handleUsernameChange}
|
||||
value={user ? user.name : ""}
|
||||
validationError= {this.state.nameValidationError}
|
||||
errorMessage= {t("validation.name-invalid")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -55,17 +78,31 @@ class UserForm extends React.Component<Props, User> {
|
||||
label={t("user.displayName")}
|
||||
onChange={this.handleDisplayNameChange}
|
||||
value={user ? user.displayName : ""}
|
||||
validationError={this.state.displayNameValidationError}
|
||||
errorMessage={t("validation.displayname-invalid")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("user.mail")}
|
||||
onChange={this.handleEmailChange}
|
||||
value={user ? user.mail : ""}
|
||||
validationError= {this.state.mailValidationError}
|
||||
errorMessage={t("validation.mail-invalid")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("user.password")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordChange}
|
||||
value={user ? user.password : ""}
|
||||
validationError={this.state.validatePasswordError}
|
||||
errorMessage={t("validation.password-invalid")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("validation.validatePassword")}
|
||||
type="password"
|
||||
onChange={this.handlePasswordValidationChange}
|
||||
value={this.state ? this.state.validatePassword : ""}
|
||||
validationError={this.state.passwordValidationError}
|
||||
errorMessage={t("validation.passwordValidation-invalid")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("user.admin")}
|
||||
@@ -77,33 +114,47 @@ class UserForm extends React.Component<Props, User> {
|
||||
onChange={this.handleActiveChange}
|
||||
checked={user ? user.active : false}
|
||||
/>
|
||||
<SubmitButton label={t("user-form.submit")} />
|
||||
<SubmitButton disabled={ButtonClickable} label={t("user-form.submit")} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
handleUsernameChange = (name: string) => {
|
||||
this.setState({ name });
|
||||
const REGEX_NAME = /^[^ ][A-z0-9\\.\-_@ ]*[^ ]$/;
|
||||
this.setState( {nameValidationError: !REGEX_NAME.test(name), user : {...this.state.user, name} } );
|
||||
};
|
||||
|
||||
handleDisplayNameChange = (displayName: string) => {
|
||||
this.setState({ displayName });
|
||||
const REGEX_NAME = /^[^ ][A-z0-9\\.\-_@ ]*[^ ]$/;
|
||||
this.setState({displayNameValidationError: !REGEX_NAME.test(displayName), user : {...this.state.user, displayName} } );
|
||||
};
|
||||
|
||||
handleEmailChange = (mail: string) => {
|
||||
this.setState({ mail });
|
||||
const REGEX_MAIL = /^[A-z0-9][\w.-]*@[A-z0-9][\w\-\\.]*\.[A-z0-9][A-z0-9-]+$/;
|
||||
this.setState( {mailValidationError: !REGEX_MAIL.test(mail), user : {...this.state.user, mail} } );
|
||||
};
|
||||
|
||||
handlePasswordChange = (password: string) => {
|
||||
this.setState({ password });
|
||||
const validatePasswordError = !this.checkPasswords(password, this.state.validatePassword);
|
||||
this.setState( {validatePasswordError: (password.length < 6) || (password.length > 32), passwordValidationError: validatePasswordError, user : {...this.state.user, password} } );
|
||||
};
|
||||
|
||||
handlePasswordValidationChange = (validatePassword: string) => {
|
||||
const validatePasswordError = this.checkPasswords(this.state.user.password, validatePassword)
|
||||
this.setState({ validatePassword, passwordValidationError: !validatePasswordError });
|
||||
};
|
||||
|
||||
checkPasswords = (password1: string, password2: string) => {
|
||||
return (password1 === password2);
|
||||
}
|
||||
|
||||
handleAdminChange = (admin: boolean) => {
|
||||
this.setState({ admin });
|
||||
this.setState({ user : {...this.state.user, admin} });
|
||||
};
|
||||
|
||||
handleActiveChange = (active: boolean) => {
|
||||
this.setState({ active });
|
||||
this.setState({ user : {...this.state.user, active} });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { EditButton } from "../../../components/buttons";
|
||||
import type { UserEntry } from "../../types/UserEntry";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
entry: UserEntry
|
||||
};
|
||||
|
||||
class EditUserButton extends React.Component<Props> {
|
||||
render() {
|
||||
const { entry, t } = this.props;
|
||||
const link = "/users/edit/" + entry.entry.name;
|
||||
|
||||
if (!this.isEditable()) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<EditButton
|
||||
label={t("edit-user-button.label")}
|
||||
link={link}
|
||||
loading={entry.loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
isEditable = () => {
|
||||
return this.props.entry.entry._links.update;
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("users")(EditUserButton);
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import "../../../tests/enzyme";
|
||||
import "../../../tests/i18n";
|
||||
import EditUserButton from "./EditUserButton";
|
||||
|
||||
it("should render nothing, if the edit link is missing", () => {
|
||||
const entry = {
|
||||
entry: {
|
||||
_links: {}
|
||||
}
|
||||
};
|
||||
|
||||
const button = shallow(<EditUserButton entry={entry} />);
|
||||
expect(button.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the button", () => {
|
||||
const entry = {
|
||||
entry: {
|
||||
_links: {
|
||||
update: {
|
||||
href: "/users"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const button = shallow(<EditUserButton entry={entry} />);
|
||||
expect(button.text()).not.toBe("");
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as DeleteUserButton } from "./DeleteUserButton";
|
||||
export { default as EditUserButton } from "./EditUserButton";
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
deleteUser: (user: User) => void
|
||||
};
|
||||
|
||||
class DeleteUserButton extends React.Component<Props> {
|
||||
class DeleteUserNavLink extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
confirmDialog: true
|
||||
};
|
||||
@@ -54,4 +54,4 @@ class DeleteUserButton extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("users")(DeleteUserButton);
|
||||
export default translate("users")(DeleteUserNavLink);
|
||||
@@ -2,24 +2,24 @@ import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import "../../../tests/enzyme";
|
||||
import "../../../tests/i18n";
|
||||
import DeleteUserButton from "./DeleteUserButton";
|
||||
import DeleteUserNavLink from "./DeleteUserNavLink";
|
||||
|
||||
import { confirmAlert } from "../../../components/modals/ConfirmAlert";
|
||||
jest.mock("../../../components/modals/ConfirmAlert");
|
||||
|
||||
describe("DeleteUserButton", () => {
|
||||
describe("DeleteUserNavLink", () => {
|
||||
it("should render nothing, if the delete link is missing", () => {
|
||||
const user = {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const button = shallow(
|
||||
<DeleteUserButton user={user} deleteUser={() => {}} />
|
||||
const navLink = shallow(
|
||||
<DeleteUserNavLink user={user} deleteUser={() => {}} />
|
||||
);
|
||||
expect(button.text()).toBe("");
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the button", () => {
|
||||
it("should render the navLink", () => {
|
||||
const user = {
|
||||
_links: {
|
||||
delete: {
|
||||
@@ -28,13 +28,13 @@ describe("DeleteUserButton", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const button = mount(
|
||||
<DeleteUserButton user={user} deleteUser={() => {}} />
|
||||
const navLink = mount(
|
||||
<DeleteUserNavLink user={user} deleteUser={() => {}} />
|
||||
);
|
||||
expect(button.text()).not.toBe("");
|
||||
expect(navLink.text()).not.toBe("");
|
||||
});
|
||||
|
||||
it("should open the confirm dialog on button click", () => {
|
||||
it("should open the confirm dialog on navLink click", () => {
|
||||
const user = {
|
||||
_links: {
|
||||
delete: {
|
||||
@@ -43,10 +43,10 @@ describe("DeleteUserButton", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const button = mount(
|
||||
<DeleteUserButton user={user} deleteUser={() => {}} />
|
||||
const navLink = mount(
|
||||
<DeleteUserNavLink user={user} deleteUser={() => {}} />
|
||||
);
|
||||
button.find("a").simulate("click");
|
||||
navLink.find("a").simulate("click");
|
||||
|
||||
expect(confirmAlert.mock.calls.length).toBe(1);
|
||||
});
|
||||
@@ -65,14 +65,14 @@ describe("DeleteUserButton", () => {
|
||||
calledUrl = user._links.delete.href;
|
||||
}
|
||||
|
||||
const button = mount(
|
||||
<DeleteUserButton
|
||||
const navLink = mount(
|
||||
<DeleteUserNavLink
|
||||
user={user}
|
||||
confirmDialog={false}
|
||||
deleteUser={capture}
|
||||
/>
|
||||
);
|
||||
button.find("a").simulate("click");
|
||||
navLink.find("a").simulate("click");
|
||||
|
||||
expect(calledUrl).toBe("/users");
|
||||
});
|
||||
28
scm-ui/src/users/components/navLinks/EditUserNavLink.js
Normal file
28
scm-ui/src/users/components/navLinks/EditUserNavLink.js
Normal file
@@ -0,0 +1,28 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { UserEntry } from "../../types/UserEntry";
|
||||
import { NavLink } from "../../../components/navigation";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
user: UserEntry,
|
||||
editUrl: String
|
||||
};
|
||||
|
||||
class EditUserNavLink extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, editUrl } = this.props;
|
||||
|
||||
if (!this.isEditable()) {
|
||||
return null;
|
||||
}
|
||||
return <NavLink label={t("edit-user-button.label")} to={editUrl} />;
|
||||
}
|
||||
|
||||
isEditable = () => {
|
||||
return this.props.user._links.update;
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("users")(EditUserNavLink);
|
||||
27
scm-ui/src/users/components/navLinks/EditUserNavLink.test.js
Normal file
27
scm-ui/src/users/components/navLinks/EditUserNavLink.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import "../../../tests/enzyme";
|
||||
import "../../../tests/i18n";
|
||||
import EditUserNavLink from "./EditUserNavLink";
|
||||
|
||||
it("should render nothing, if the edit link is missing", () => {
|
||||
const user = {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the navLink", () => {
|
||||
const user = {
|
||||
_links: {
|
||||
update: {
|
||||
href: "/users"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navLink = shallow(<EditUserNavLink user={user} editUrl='/user/edit'/>);
|
||||
expect(navLink.text()).not.toBe("");
|
||||
});
|
||||
2
scm-ui/src/users/components/navLinks/index.js
Normal file
2
scm-ui/src/users/components/navLinks/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
|
||||
export { default as EditUserNavLink } from "./EditUserNavLink";
|
||||
@@ -6,8 +6,10 @@ import type { User } from "../types/User";
|
||||
import type { History } from "history";
|
||||
import { createUser } from "../modules/users";
|
||||
import { Page } from "../../components/layout";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
addUser: (user: User, callback?: () => void) => void,
|
||||
loading?: boolean,
|
||||
error?: Error,
|
||||
@@ -25,13 +27,12 @@ class AddUser extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error } = this.props;
|
||||
const { t, loading, error } = this.props;
|
||||
|
||||
// TODO i18n
|
||||
return (
|
||||
<Page
|
||||
title="Create User"
|
||||
subtitle="Create a new user"
|
||||
title={t("add-user.title")}
|
||||
subtitle={t("add-user.subtitle")}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
@@ -59,4 +60,4 @@ const mapStateToProps = (state, ownProps) => {
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AddUser);
|
||||
)(translate("users")(AddUser));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import {connect} from "react-redux";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import UserForm from "./../components/UserForm";
|
||||
import type { User } from "../types/User";
|
||||
import { modifyUser } from "../modules/users";
|
||||
import type { History } from "history";
|
||||
import type {User} from "../types/User";
|
||||
import {modifyUser} from "../modules/users";
|
||||
import type {History} from "history";
|
||||
|
||||
type Props = {
|
||||
user: User,
|
||||
|
||||
@@ -12,10 +12,13 @@ import { fetchUser, deleteUser } from "../modules/users";
|
||||
import Loading from "../../components/Loading";
|
||||
|
||||
import { Navigation, Section, NavLink } from "../../components/navigation";
|
||||
import { DeleteUserButton } from "./../components/buttons";
|
||||
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
||||
import ErrorPage from "../../components/ErrorPage";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
name: string,
|
||||
userEntry?: UserEntry,
|
||||
match: any,
|
||||
@@ -49,7 +52,7 @@ class SingleUser extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userEntry } = this.props;
|
||||
const { t, userEntry } = this.props;
|
||||
|
||||
if (!userEntry || userEntry.loading) {
|
||||
return <Loading />;
|
||||
@@ -58,8 +61,8 @@ class SingleUser extends React.Component<Props> {
|
||||
if (userEntry.error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title="Error"
|
||||
subtitle="Unknown user error"
|
||||
title={t("single-user.error-title")}
|
||||
subtitle={t("single-user.error-subtitle")}
|
||||
error={userEntry.error}
|
||||
/>
|
||||
);
|
||||
@@ -68,8 +71,6 @@ class SingleUser extends React.Component<Props> {
|
||||
const user = userEntry.entry;
|
||||
const url = this.matchedUrl();
|
||||
|
||||
// TODO i18n
|
||||
|
||||
return (
|
||||
<Page title={user.displayName}>
|
||||
<div className="columns">
|
||||
@@ -82,13 +83,13 @@ class SingleUser extends React.Component<Props> {
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label="Navigation">
|
||||
<NavLink to={`${url}`} label="Information" />
|
||||
<NavLink to={`${url}/edit`} label="Edit" />
|
||||
<Section label={t("single-user.navigation-label")}>
|
||||
<NavLink to={`${url}`} label={t("single-user.information-label")} />
|
||||
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
|
||||
</Section>
|
||||
<Section label="Actions">
|
||||
<DeleteUserButton user={user} deleteUser={this.deleteUser} />
|
||||
<NavLink to="/users" label="Back" />
|
||||
<Section label={t("single-user.actions-label")}>
|
||||
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
|
||||
<NavLink to="/users" label={t("single-user.back-label")} />
|
||||
</Section>
|
||||
</Navigation>
|
||||
</div>
|
||||
@@ -125,4 +126,4 @@ const mapDispatchToProps = dispatch => {
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SingleUser);
|
||||
)(translate("users")(SingleUser));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { apiClient } from "../../apiclient";
|
||||
import type { User } from "../types/User";
|
||||
import type { UserEntry } from "../types/UserEntry";
|
||||
import { Dispatch } from "redux";
|
||||
import { combineReducers, Dispatch } from "redux";
|
||||
import type { Action } from "../../types/Action";
|
||||
import type { PageCollectionStateSlice } from "../../types/Collection";
|
||||
|
||||
@@ -22,7 +22,7 @@ export const MODIFY_USER_PENDING = "scm/users/MODIFY_USER_PENDING";
|
||||
export const MODIFY_USER_SUCCESS = "scm/users/MODIFY_USER_SUCCESS";
|
||||
export const MODIFY_USER_FAILURE = "scm/users/MODIFY_USER_FAILURE";
|
||||
|
||||
export const DELETE_USER = "scm/users/DELETE";
|
||||
export const DELETE_USER_PENDING = "scm/users/DELETE_PENDING";
|
||||
export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS";
|
||||
export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE";
|
||||
|
||||
@@ -30,6 +30,8 @@ const USERS_URL = "users";
|
||||
|
||||
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
||||
|
||||
//TODO i18n
|
||||
|
||||
//fetch users
|
||||
|
||||
export function fetchUsers() {
|
||||
@@ -241,7 +243,7 @@ export function deleteUser(user: User, callback?: () => void) {
|
||||
|
||||
export function deleteUserPending(user: User): Action {
|
||||
return {
|
||||
type: DELETE_USER,
|
||||
type: DELETE_USER_PENDING,
|
||||
payload: user
|
||||
};
|
||||
}
|
||||
@@ -300,7 +302,8 @@ function extractUsersByNames(
|
||||
}
|
||||
return usersByNames;
|
||||
}
|
||||
function deleteUserInUsersByNames(users: {}, userName: any) {
|
||||
|
||||
function deleteUserInUsersByNames(users: {}, userName: string) {
|
||||
let newUsers = {};
|
||||
for (let username in users) {
|
||||
if (username !== userName) newUsers[username] = users[username];
|
||||
@@ -308,7 +311,7 @@ function deleteUserInUsersByNames(users: {}, userName: any) {
|
||||
return newUsers;
|
||||
}
|
||||
|
||||
function deleteUserInEntries(users: [], userName: any) {
|
||||
function deleteUserInEntries(users: [], userName: string) {
|
||||
let newUsers = [];
|
||||
for (let user of users) {
|
||||
if (user !== userName) newUsers.push(user);
|
||||
@@ -318,130 +321,87 @@ function deleteUserInEntries(users: [], userName: any) {
|
||||
|
||||
const reducerByName = (state: any, username: string, newUserState: any) => {
|
||||
const newUsersByNames = {
|
||||
...state.byNames,
|
||||
...state,
|
||||
[username]: newUserState
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
byNames: newUsersByNames
|
||||
};
|
||||
return newUsersByNames;
|
||||
};
|
||||
|
||||
export default function reducer(state: any = {}, action: any = {}) {
|
||||
function listReducer(state: any = {}, action: any = {}) {
|
||||
switch (action.type) {
|
||||
// fetch user list cases
|
||||
// Fetch all users actions
|
||||
case FETCH_USERS_PENDING:
|
||||
return {
|
||||
...state,
|
||||
list: {
|
||||
loading: true
|
||||
}
|
||||
};
|
||||
case FETCH_USERS_SUCCESS:
|
||||
const users = action.payload._embedded.users;
|
||||
const userNames = users.map(user => user.name);
|
||||
const byNames = extractUsersByNames(users, userNames, state.byNames);
|
||||
return {
|
||||
...state,
|
||||
list: {
|
||||
error: null,
|
||||
loading: false,
|
||||
entries: userNames,
|
||||
loading: false,
|
||||
entry: {
|
||||
userCreatePermission: action.payload._links.create ? true : false,
|
||||
page: action.payload.page,
|
||||
pageTotal: action.payload.pageTotal,
|
||||
_links: action.payload._links
|
||||
}
|
||||
},
|
||||
byNames
|
||||
};
|
||||
case FETCH_USERS_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
list: {
|
||||
...state.users,
|
||||
loading: false,
|
||||
error: action.payload.error
|
||||
}
|
||||
};
|
||||
// Fetch single user cases
|
||||
// Delete single user actions
|
||||
case DELETE_USER_SUCCESS:
|
||||
const newUserEntries = deleteUserInEntries(
|
||||
state.entries,
|
||||
action.payload.name
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
entries: newUserEntries
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function byNamesReducer(state: any = {}, action: any = {}) {
|
||||
switch (action.type) {
|
||||
// Fetch all users actions
|
||||
case FETCH_USERS_SUCCESS:
|
||||
const users = action.payload._embedded.users;
|
||||
const userNames = users.map(user => user.name);
|
||||
const byNames = extractUsersByNames(users, userNames, state.byNames);
|
||||
return {
|
||||
...byNames
|
||||
};
|
||||
|
||||
// Fetch single user actions
|
||||
case FETCH_USER_PENDING:
|
||||
return reducerByName(state, action.payload.name, {
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
case FETCH_USER_SUCCESS:
|
||||
return reducerByName(state, action.payload.name, {
|
||||
loading: false,
|
||||
error: null,
|
||||
entry: action.payload
|
||||
});
|
||||
|
||||
case FETCH_USER_FAILURE:
|
||||
return reducerByName(state, action.payload.username, {
|
||||
loading: false,
|
||||
error: action.payload.error
|
||||
});
|
||||
|
||||
// Delete single user cases
|
||||
case DELETE_USER:
|
||||
return reducerByName(state, action.payload.name, {
|
||||
loading: true,
|
||||
error: null,
|
||||
entry: action.payload
|
||||
});
|
||||
|
||||
case DELETE_USER_SUCCESS:
|
||||
const newUserByNames = deleteUserInUsersByNames(state.byNames, [
|
||||
action.payload.name
|
||||
]);
|
||||
const newUserEntries = deleteUserInEntries(state.list.entries, [
|
||||
action.payload.name
|
||||
]);
|
||||
return {
|
||||
...state,
|
||||
list: {
|
||||
...state.list,
|
||||
entries: newUserEntries
|
||||
},
|
||||
byNames: newUserByNames
|
||||
};
|
||||
|
||||
case DELETE_USER_FAILURE:
|
||||
return reducerByName(state, action.payload.user.name, {
|
||||
loading: false,
|
||||
error: action.payload.error,
|
||||
entry: action.payload.user
|
||||
});
|
||||
|
||||
// Add single user cases
|
||||
case CREATE_USER_PENDING:
|
||||
return {
|
||||
...state,
|
||||
create: {
|
||||
loading: true
|
||||
}
|
||||
};
|
||||
case CREATE_USER_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
create: {
|
||||
loading: false
|
||||
}
|
||||
};
|
||||
case CREATE_USER_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
create: {
|
||||
loading: false,
|
||||
error: action.payload
|
||||
}
|
||||
};
|
||||
|
||||
// Update single user cases
|
||||
// Update single user actions
|
||||
case MODIFY_USER_PENDING:
|
||||
return reducerByName(state, action.payload.name, {
|
||||
loading: true
|
||||
@@ -454,6 +414,27 @@ export default function reducer(state: any = {}, action: any = {}) {
|
||||
return reducerByName(state, action.payload.user.name, {
|
||||
error: action.payload.error
|
||||
});
|
||||
|
||||
// Delete single user actions
|
||||
case DELETE_USER_PENDING:
|
||||
return reducerByName(state, action.payload.name, {
|
||||
loading: true,
|
||||
error: null,
|
||||
entry: action.payload
|
||||
});
|
||||
case DELETE_USER_SUCCESS:
|
||||
const newUserByNames = deleteUserInUsersByNames(
|
||||
state,
|
||||
action.payload.name
|
||||
);
|
||||
return newUserByNames;
|
||||
|
||||
case DELETE_USER_FAILURE:
|
||||
return reducerByName(state, action.payload.user.name, {
|
||||
loading: false,
|
||||
error: action.payload.error,
|
||||
entry: action.payload.user
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -489,3 +470,31 @@ export const isPermittedToCreateUsers = (state: Object): boolean => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
function createReducer(state: any = {}, action: any = {}) {
|
||||
switch (action.type) {
|
||||
case CREATE_USER_PENDING:
|
||||
return {
|
||||
...state,
|
||||
loading: true
|
||||
};
|
||||
case CREATE_USER_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false
|
||||
};
|
||||
case CREATE_USER_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: action.payload
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
list: listReducer,
|
||||
byNames: byNamesReducer,
|
||||
create: createReducer
|
||||
});
|
||||
|
||||
@@ -3,48 +3,46 @@ import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import {
|
||||
FETCH_USERS_PENDING,
|
||||
FETCH_USERS_SUCCESS,
|
||||
fetchUsers,
|
||||
FETCH_USERS_FAILURE,
|
||||
createUserPending,
|
||||
import reducer, {
|
||||
CREATE_USER_FAILURE,
|
||||
CREATE_USER_PENDING,
|
||||
CREATE_USER_SUCCESS,
|
||||
CREATE_USER_FAILURE,
|
||||
modifyUser,
|
||||
MODIFY_USER_PENDING,
|
||||
MODIFY_USER_FAILURE,
|
||||
MODIFY_USER_SUCCESS,
|
||||
deleteUserPending,
|
||||
deleteUserFailure,
|
||||
DELETE_USER,
|
||||
DELETE_USER_SUCCESS,
|
||||
createUser,
|
||||
createUserFailure,
|
||||
createUserPending,
|
||||
createUserSuccess,
|
||||
DELETE_USER_FAILURE,
|
||||
DELETE_USER_PENDING,
|
||||
DELETE_USER_SUCCESS,
|
||||
deleteUser,
|
||||
fetchUsersFailure,
|
||||
fetchUsersSuccess,
|
||||
fetchUser,
|
||||
deleteUserFailure,
|
||||
deleteUserPending,
|
||||
deleteUserSuccess,
|
||||
FETCH_USER_FAILURE,
|
||||
FETCH_USER_PENDING,
|
||||
FETCH_USER_SUCCESS,
|
||||
FETCH_USER_FAILURE,
|
||||
createUser,
|
||||
createUserSuccess,
|
||||
createUserFailure,
|
||||
modifyUserPending,
|
||||
modifyUserSuccess,
|
||||
modifyUserFailure,
|
||||
fetchUserSuccess,
|
||||
deleteUserSuccess,
|
||||
fetchUsersPending,
|
||||
fetchUserPending,
|
||||
FETCH_USERS_FAILURE,
|
||||
FETCH_USERS_PENDING,
|
||||
FETCH_USERS_SUCCESS,
|
||||
fetchUser,
|
||||
fetchUserFailure,
|
||||
fetchUserPending,
|
||||
fetchUsers,
|
||||
fetchUsersFailure,
|
||||
fetchUsersPending,
|
||||
fetchUsersSuccess,
|
||||
fetchUserSuccess,
|
||||
selectListAsCollection,
|
||||
isPermittedToCreateUsers
|
||||
isPermittedToCreateUsers,
|
||||
MODIFY_USER_FAILURE,
|
||||
MODIFY_USER_PENDING,
|
||||
MODIFY_USER_SUCCESS,
|
||||
modifyUser,
|
||||
modifyUserFailure,
|
||||
modifyUserPending,
|
||||
modifyUserSuccess
|
||||
} from "./users";
|
||||
|
||||
import reducer from "./users";
|
||||
|
||||
const userZaphod = {
|
||||
active: true,
|
||||
admin: true,
|
||||
@@ -91,28 +89,6 @@ const userFord = {
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -309,7 +285,7 @@ describe("users fetch()", () => {
|
||||
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions.length).toBe(2);
|
||||
expect(actions[0].type).toEqual(DELETE_USER);
|
||||
expect(actions[0].type).toEqual(DELETE_USER_PENDING);
|
||||
expect(actions[0].payload).toBe(userZaphod);
|
||||
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
|
||||
});
|
||||
@@ -339,7 +315,7 @@ describe("users fetch()", () => {
|
||||
const store = mockStore({});
|
||||
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(DELETE_USER);
|
||||
expect(actions[0].type).toEqual(DELETE_USER_PENDING);
|
||||
expect(actions[0].payload).toBe(userZaphod);
|
||||
expect(actions[1].type).toEqual(DELETE_USER_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
@@ -381,9 +357,44 @@ describe("users reducer", () => {
|
||||
expect(newState.list.entry.userCreatePermission).toBeTruthy();
|
||||
});
|
||||
|
||||
test("should update state correctly according to DELETE_USER action", () => {
|
||||
it("should set error when fetching users failed", () => {
|
||||
const oldState = {
|
||||
list: {
|
||||
loading: true
|
||||
}
|
||||
};
|
||||
|
||||
const error = new Error("kaputt");
|
||||
|
||||
const newState = reducer(oldState, fetchUsersFailure("url.com", error));
|
||||
expect(newState.list.loading).toBeFalsy();
|
||||
expect(newState.list.error).toEqual(error);
|
||||
});
|
||||
|
||||
it("should set userCreatePermission to true if update link is present", () => {
|
||||
const newState = reducer({}, fetchUsersSuccess(responseBody));
|
||||
|
||||
expect(newState.list.entry.userCreatePermission).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not replace whole byNames map when fetching users", () => {
|
||||
const oldState = {
|
||||
byNames: {
|
||||
ford: {
|
||||
entry: userFord
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(oldState, fetchUsersSuccess(responseBody));
|
||||
expect(newState.byNames["zaphod"]).toBeDefined();
|
||||
expect(newState.byNames["ford"]).toBeDefined();
|
||||
});
|
||||
|
||||
test("should update state correctly according to DELETE_USER_PENDING action", () => {
|
||||
const state = {
|
||||
usersByNames: {
|
||||
byNames: {
|
||||
zaphod: {
|
||||
loading: false,
|
||||
error: null,
|
||||
@@ -396,11 +407,11 @@ describe("users reducer", () => {
|
||||
const zaphod = newState.byNames["zaphod"];
|
||||
expect(zaphod.loading).toBeTruthy();
|
||||
expect(zaphod.entry).toBe(userZaphod);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not effect other users if one user will be deleted", () => {
|
||||
it("should not effect other users if one user will be deleted", () => {
|
||||
const state = {
|
||||
usersByNames: {
|
||||
byNames: {
|
||||
zaphod: {
|
||||
loading: false,
|
||||
error: null,
|
||||
@@ -413,13 +424,13 @@ describe("users reducer", () => {
|
||||
};
|
||||
|
||||
const newState = reducer(state, deleteUserPending(userZaphod));
|
||||
const ford = newState.usersByNames["ford"];
|
||||
const ford = newState.byNames["ford"];
|
||||
expect(ford.loading).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the error of user which could not be deleted", () => {
|
||||
it("should set the error of user which could not be deleted", () => {
|
||||
const state = {
|
||||
usersByNames: {
|
||||
byNames: {
|
||||
zaphod: {
|
||||
loading: true,
|
||||
entry: userZaphod
|
||||
@@ -432,11 +443,11 @@ describe("users reducer", () => {
|
||||
const zaphod = newState.byNames["zaphod"];
|
||||
expect(zaphod.loading).toBeFalsy();
|
||||
expect(zaphod.error).toBe(error);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not effect other users if one user could not be deleted", () => {
|
||||
it("should not effect other users if one user could not be deleted", () => {
|
||||
const state = {
|
||||
usersByNames: {
|
||||
byNames: {
|
||||
zaphod: {
|
||||
loading: false,
|
||||
error: null,
|
||||
@@ -450,83 +461,97 @@ describe("users reducer", () => {
|
||||
|
||||
const error = new Error("error");
|
||||
const newState = reducer(state, deleteUserFailure(userZaphod, error));
|
||||
const ford = newState.usersByNames["ford"];
|
||||
const ford = newState.byNames["ford"];
|
||||
expect(ford.loading).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not replace whole byNames map when fetching users", () => {
|
||||
const oldState = {
|
||||
it("should remove user from state when delete succeeds", () => {
|
||||
const state = {
|
||||
list: {
|
||||
entries: ["ford", "zaphod"]
|
||||
},
|
||||
byNames: {
|
||||
zaphod: {
|
||||
loading: true,
|
||||
error: null,
|
||||
entry: userZaphod
|
||||
},
|
||||
ford: {
|
||||
loading: true,
|
||||
error: null,
|
||||
entry: userFord
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(oldState, fetchUsersSuccess(responseBody));
|
||||
const newState = reducer(state, deleteUserSuccess(userFord));
|
||||
expect(newState.byNames["zaphod"]).toBeDefined();
|
||||
expect(newState.byNames["ford"]).toBeDefined();
|
||||
});
|
||||
expect(newState.byNames["ford"]).toBeFalsy();
|
||||
expect(newState.list.entries).toEqual(["zaphod"]);
|
||||
});
|
||||
|
||||
it("should set userCreatePermission to true if create link is present", () => {
|
||||
it("should set userCreatePermission to true if create link is present", () => {
|
||||
const newState = reducer({}, fetchUsersSuccess(responseBody));
|
||||
|
||||
expect(newState.list.entry.userCreatePermission).toBeTruthy();
|
||||
});
|
||||
expect(newState.list.entries).toEqual(["zaphod", "ford"]);
|
||||
expect(newState.byNames["ford"]).toBeTruthy();
|
||||
expect(newState.byNames["zaphod"]).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should update state correctly according to CREATE_USER_PENDING action", () => {
|
||||
it("should update state correctly according to CREATE_USER_PENDING action", () => {
|
||||
const newState = reducer({}, createUserPending(userZaphod));
|
||||
expect(newState.create.loading).toBeTruthy();
|
||||
expect(newState.create.error).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state correctly according to CREATE_USER_SUCCESS action", () => {
|
||||
const newState = reducer({ loading: true }, createUserSuccess());
|
||||
it("should update state correctly according to CREATE_USER_SUCCESS action", () => {
|
||||
const newState = reducer({ create: { loading: true } }, createUserSuccess());
|
||||
expect(newState.create.loading).toBeFalsy();
|
||||
expect(newState.create.error).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the loading to false and the error if user could not be created", () => {
|
||||
it("should set the loading to false and the error if user could not be created", () => {
|
||||
const newState = reducer(
|
||||
{ loading: true, error: null },
|
||||
{ create: { loading: true, error: null } },
|
||||
createUserFailure(userFord, new Error("kaputt kaputt"))
|
||||
);
|
||||
expect(newState.create.loading).toBeFalsy();
|
||||
expect(newState.create.error).toEqual(new Error("kaputt kaputt"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state according to FETCH_USER_PENDING action", () => {
|
||||
it("should update state according to FETCH_USER_PENDING action", () => {
|
||||
const newState = reducer({}, fetchUserPending("zaphod"));
|
||||
expect(newState.byNames["zaphod"].loading).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not affect users state", () => {
|
||||
it("should not affect list state", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
users: {
|
||||
list: {
|
||||
entries: ["ford"]
|
||||
}
|
||||
},
|
||||
fetchUserPending("zaphod")
|
||||
);
|
||||
expect(newState.byNames["zaphod"].loading).toBeTruthy();
|
||||
expect(newState.users.entries).toEqual(["ford"]);
|
||||
});
|
||||
expect(newState.list.entries).toEqual(["ford"]);
|
||||
});
|
||||
|
||||
it("should update state according to FETCH_USER_FAILURE action", () => {
|
||||
it("should update state according to FETCH_USER_FAILURE action", () => {
|
||||
const error = new Error("kaputt!");
|
||||
const newState = reducer({}, fetchUserFailure(userFord.name, error));
|
||||
expect(newState.byNames["ford"].error).toBe(error);
|
||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state according to FETCH_USER_SUCCESS action", () => {
|
||||
it("should update state according to FETCH_USER_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchUserSuccess(userFord));
|
||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
||||
expect(newState.byNames["ford"].entry).toBe(userFord);
|
||||
});
|
||||
});
|
||||
|
||||
it("should affect users state nor the state of other users", () => {
|
||||
it("should affect users state nor the state of other users", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
list: {
|
||||
@@ -538,48 +563,59 @@ describe("users reducer", () => {
|
||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
||||
expect(newState.byNames["ford"].entry).toBe(userFord);
|
||||
expect(newState.list.entries).toEqual(["zaphod"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state according to MODIFY_USER_PENDING action", () => {
|
||||
it("should update state according to MODIFY_USER_PENDING action", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
byNames: {
|
||||
ford: {
|
||||
error: new Error("something"),
|
||||
entry: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifyUserPending(userFord)
|
||||
);
|
||||
expect(newState.byNames["ford"].loading).toBeTruthy();
|
||||
expect(newState.byNames["ford"].error).toBeFalsy();
|
||||
expect(newState.byNames["ford"].entry).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state according to MODIFY_USER_SUCCESS action", () => {
|
||||
it("should update state according to MODIFY_USER_SUCCESS action", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
byNames: {
|
||||
ford: {
|
||||
loading: true,
|
||||
error: new Error("something"),
|
||||
entry: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifyUserSuccess(userFord)
|
||||
);
|
||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
||||
expect(newState.byNames["ford"].error).toBeFalsy();
|
||||
expect(newState.byNames["ford"].entry).toBe(userFord);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state according to MODIFY_USER_SUCCESS action", () => {
|
||||
it("should update state according to MODIFY_USER_SUCCESS action", () => {
|
||||
const error = new Error("something went wrong");
|
||||
const newState = reducer(
|
||||
{
|
||||
byNames: {
|
||||
ford: {
|
||||
loading: true,
|
||||
entry: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifyUserFailure(userFord, error)
|
||||
);
|
||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
||||
expect(newState.byNames["ford"].error).toBe(error);
|
||||
expect(newState.byNames["ford"].entry).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import javax.ws.rs.FormParam;
|
||||
import java.util.List;
|
||||
|
||||
public class AuthenticationRequestDto {
|
||||
|
||||
@FormParam("grant_type")
|
||||
@JsonProperty("grant_type")
|
||||
private String grantType;
|
||||
|
||||
@FormParam("username")
|
||||
private String username;
|
||||
|
||||
@FormParam("password")
|
||||
private String password;
|
||||
|
||||
@FormParam("cookie")
|
||||
private boolean cookie;
|
||||
|
||||
@FormParam("scope")
|
||||
private List<String> scope;
|
||||
|
||||
public String getGrantType() {
|
||||
return grantType;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public boolean isCookie() {
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public List<String> getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(grantType), "grant_type parameter is required");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username parameter is required");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password parameter is required");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
@@ -26,11 +23,7 @@ import javax.ws.rs.core.Response;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by masuewer on 04.07.18.
|
||||
*/
|
||||
@Path(AuthenticationResource.PATH)
|
||||
public class AuthenticationResource {
|
||||
|
||||
@@ -61,7 +54,7 @@ public class AuthenticationResource {
|
||||
public Response authenticateViaForm(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
@BeanParam AuthenticationRequest authentication
|
||||
@BeanParam AuthenticationRequestDto authentication
|
||||
) {
|
||||
return authenticate(request, response, authentication);
|
||||
}
|
||||
@@ -78,7 +71,7 @@ public class AuthenticationResource {
|
||||
public Response authenticateViaJSONBody(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
AuthenticationRequest authentication
|
||||
AuthenticationRequestDto authentication
|
||||
) {
|
||||
return authenticate(request, response, authentication);
|
||||
}
|
||||
@@ -86,7 +79,7 @@ public class AuthenticationResource {
|
||||
private Response authenticate(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationRequest authentication
|
||||
AuthenticationRequestDto authentication
|
||||
) {
|
||||
authentication.validate();
|
||||
|
||||
@@ -180,51 +173,6 @@ public class AuthenticationResource {
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
public static class AuthenticationRequest {
|
||||
|
||||
@FormParam("grant_type")
|
||||
@JsonProperty("grant_type")
|
||||
private String grantType;
|
||||
|
||||
@FormParam("username")
|
||||
private String username;
|
||||
|
||||
@FormParam("password")
|
||||
private String password;
|
||||
|
||||
@FormParam("cookie")
|
||||
private boolean cookie;
|
||||
|
||||
@FormParam("scope")
|
||||
private List<String> scope;
|
||||
|
||||
public String getGrantType() {
|
||||
return grantType;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public boolean isCookie() {
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public List<String> getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(grantType), "grant_type parameter is required");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username parameter is required");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password parameter is required");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Response handleFailedAuthentication(HttpServletRequest request,
|
||||
AuthenticationException ex, Response.Status status,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
@@ -21,6 +22,11 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@VisibleForTesting
|
||||
void setResourceLinks(ResourceLinks resourceLinks) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void removePassword(@MappingTarget UserDto target) {
|
||||
target.setPassword(UserResource.DUMMY_PASSWORT);
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenBuilder;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserException;
|
||||
import sonia.scm.user.UserManager;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||
)
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AuthenticationResourceTest {
|
||||
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
|
||||
@Mock
|
||||
private AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
|
||||
@Mock
|
||||
private AccessTokenBuilder accessTokenBuilder;
|
||||
|
||||
private AccessTokenCookieIssuer cookieIssuer = new AccessTokenCookieIssuer(mock(ScmConfiguration.class));
|
||||
|
||||
private static final String AUTH_JSON_TRILLIAN = "{\n" +
|
||||
"\t\"cookie\": true,\n" +
|
||||
"\t\"grant_type\": \"password\",\n" +
|
||||
"\t\"username\": \"trillian\",\n" +
|
||||
"\t\"password\": \"secret\"\n" +
|
||||
"}";
|
||||
|
||||
private static final String AUTH_JSON_TRILLIAN_WRONG_PW = "{\n" +
|
||||
"\t\"cookie\": true,\n" +
|
||||
"\t\"grant_type\": \"password\",\n" +
|
||||
"\t\"username\": \"trillian\",\n" +
|
||||
"\t\"password\": \"justWrong\"\n" +
|
||||
"}";
|
||||
|
||||
private static final String AUTH_JSON_NOT_EXISTING_USER = "{\n" +
|
||||
"\t\"cookie\": true,\n" +
|
||||
"\t\"grant_type\": \"password\",\n" +
|
||||
"\t\"username\": \"iDoNotExist\",\n" +
|
||||
"\t\"password\": \"doesNotMatter\"\n" +
|
||||
"}";
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
AuthenticationResource authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
||||
dispatcher.getRegistry().addSingletonResource(authenticationResource);
|
||||
|
||||
AccessToken accessToken = mock(AccessToken.class);
|
||||
when(accessToken.getExpiration()).thenReturn(new Date(Long.MAX_VALUE));
|
||||
when(accessTokenBuilder.build()).thenReturn(accessToken);
|
||||
|
||||
when(accessTokenBuilderFactory.create()).thenReturn(accessTokenBuilder);
|
||||
|
||||
HttpServletRequest servletRequest = mock(HttpServletRequest.class);
|
||||
ResteasyProviderFactory.getContextDataMap().put(HttpServletRequest.class, servletRequest);
|
||||
|
||||
HttpServletResponse servletResponse = mock(HttpServletResponse.class);
|
||||
ResteasyProviderFactory.getContextDataMap().put(HttpServletResponse.class, servletResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAuthCorrectly() throws URISyntaxException {
|
||||
|
||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAuthUserWithWrongPassword() throws URISyntaxException {
|
||||
|
||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_TRILLIAN_WRONG_PW);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAuthNonexistingUser() throws URISyntaxException {
|
||||
|
||||
MockHttpRequest request = getMockHttpRequest(AUTH_JSON_NOT_EXISTING_USER);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
|
||||
}
|
||||
|
||||
private MockHttpRequest getMockHttpRequest(String jsonPayload) throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/" + AuthenticationResource.PATH + "/access_token");
|
||||
|
||||
request.content(jsonPayload.getBytes());
|
||||
request.contentType(MediaType.APPLICATION_JSON_TYPE);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -39,8 +39,6 @@ import static org.mockito.Mockito.*;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
@SubjectAware(
|
||||
// username = "trillian",
|
||||
// password = "secret",
|
||||
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||
)
|
||||
public class MeResourceTest {
|
||||
@@ -50,6 +48,8 @@ public class MeResourceTest {
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
|
||||
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||
@Mock
|
||||
private UriInfo uriInfo;
|
||||
@Mock
|
||||
@@ -67,9 +67,10 @@ public class MeResourceTest {
|
||||
public void prepareEnvironment() throws IOException, UserException {
|
||||
initMocks(this);
|
||||
createDummyUser("trillian");
|
||||
doNothing().when(userManager).create(userCaptor.capture());
|
||||
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
|
||||
doNothing().when(userManager).modify(userCaptor.capture());
|
||||
doNothing().when(userManager).delete(userCaptor.capture());
|
||||
userToDtoMapper.setResourceLinks(resourceLinks);
|
||||
MeResource meResource = new MeResource(userToDtoMapper, userManager);
|
||||
dispatcher.getRegistry().addSingletonResource(meResource);
|
||||
when(uriInfo.getBaseUri()).thenReturn(URI.create("/"));
|
||||
|
||||
Reference in New Issue
Block a user