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": {
|
"user-form": {
|
||||||
"submit": "Submit"
|
"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
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: string,
|
label?: string,
|
||||||
@@ -7,7 +8,9 @@ type Props = {
|
|||||||
value?: string,
|
value?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
autofocus?: boolean,
|
autofocus?: boolean,
|
||||||
onChange: string => void
|
onChange: string => void,
|
||||||
|
validationError: boolean,
|
||||||
|
errorMessage: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputField extends React.Component<Props> {
|
class InputField extends React.Component<Props> {
|
||||||
@@ -37,8 +40,9 @@ class InputField extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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 (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
{this.renderLabel()}
|
{this.renderLabel()}
|
||||||
@@ -47,13 +51,17 @@ class InputField extends React.Component<Props> {
|
|||||||
ref={input => {
|
ref={input => {
|
||||||
this.field = input;
|
this.field = input;
|
||||||
}}
|
}}
|
||||||
className="input"
|
className={ classNames(
|
||||||
|
"input",
|
||||||
|
errorView
|
||||||
|
)}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.handleInput}
|
onChange={this.handleInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{helper}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import {translate} from "react-i18next";
|
||||||
import type { User } from "../types/User";
|
import type {User} from "../types/User";
|
||||||
import { InputField, Checkbox } from "../../components/forms";
|
import {Checkbox, InputField} from "../../components/forms";
|
||||||
import { SubmitButton } from "../../components/buttons";
|
import {SubmitButton} from "../../components/buttons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
submitForm: User => void,
|
submitForm: User => void,
|
||||||
@@ -11,33 +11,54 @@ type Props = {
|
|||||||
t: string => string
|
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) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
name: "",
|
user: {
|
||||||
displayName: "",
|
name: "",
|
||||||
mail: "",
|
displayName: "",
|
||||||
password: "",
|
mail: "",
|
||||||
admin: false,
|
password: "",
|
||||||
active: false,
|
admin: false,
|
||||||
_links: {}
|
active: false,
|
||||||
|
_links: {}
|
||||||
|
},
|
||||||
|
mailValidationError: false,
|
||||||
|
displayNameValidationError: false,
|
||||||
|
nameValidationError: false,
|
||||||
|
passwordValidationError: false,
|
||||||
|
validatePasswordError: false,
|
||||||
|
validatePassword: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.setState({ ...this.props.user });
|
this.setState({ user: {...this.props.user} });
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = (event: Event) => {
|
submit = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.props.submitForm(this.state);
|
this.props.submitForm(this.state.user);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
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;
|
let nameField = null;
|
||||||
if (!this.props.user) {
|
if (!this.props.user) {
|
||||||
nameField = (
|
nameField = (
|
||||||
@@ -45,6 +66,8 @@ class UserForm extends React.Component<Props, User> {
|
|||||||
label={t("user.name")}
|
label={t("user.name")}
|
||||||
onChange={this.handleUsernameChange}
|
onChange={this.handleUsernameChange}
|
||||||
value={user ? user.name : ""}
|
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")}
|
label={t("user.displayName")}
|
||||||
onChange={this.handleDisplayNameChange}
|
onChange={this.handleDisplayNameChange}
|
||||||
value={user ? user.displayName : ""}
|
value={user ? user.displayName : ""}
|
||||||
|
validationError={this.state.displayNameValidationError}
|
||||||
|
errorMessage={t("validation.displayname-invalid")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("user.mail")}
|
label={t("user.mail")}
|
||||||
onChange={this.handleEmailChange}
|
onChange={this.handleEmailChange}
|
||||||
value={user ? user.mail : ""}
|
value={user ? user.mail : ""}
|
||||||
|
validationError= {this.state.mailValidationError}
|
||||||
|
errorMessage={t("validation.mail-invalid")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("user.password")}
|
label={t("user.password")}
|
||||||
type="password"
|
type="password"
|
||||||
onChange={this.handlePasswordChange}
|
onChange={this.handlePasswordChange}
|
||||||
value={user ? user.password : ""}
|
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
|
<Checkbox
|
||||||
label={t("user.admin")}
|
label={t("user.admin")}
|
||||||
@@ -77,33 +114,47 @@ class UserForm extends React.Component<Props, User> {
|
|||||||
onChange={this.handleActiveChange}
|
onChange={this.handleActiveChange}
|
||||||
checked={user ? user.active : false}
|
checked={user ? user.active : false}
|
||||||
/>
|
/>
|
||||||
<SubmitButton label={t("user-form.submit")} />
|
<SubmitButton disabled={ButtonClickable} label={t("user-form.submit")} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleUsernameChange = (name: string) => {
|
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) => {
|
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) => {
|
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) => {
|
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) => {
|
handleAdminChange = (admin: boolean) => {
|
||||||
this.setState({ admin });
|
this.setState({ user : {...this.state.user, admin} });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleActiveChange = (active: boolean) => {
|
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
|
deleteUser: (user: User) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeleteUserButton extends React.Component<Props> {
|
class DeleteUserNavLink extends React.Component<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
confirmDialog: true
|
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 { mount, shallow } from "enzyme";
|
||||||
import "../../../tests/enzyme";
|
import "../../../tests/enzyme";
|
||||||
import "../../../tests/i18n";
|
import "../../../tests/i18n";
|
||||||
import DeleteUserButton from "./DeleteUserButton";
|
import DeleteUserNavLink from "./DeleteUserNavLink";
|
||||||
|
|
||||||
import { confirmAlert } from "../../../components/modals/ConfirmAlert";
|
import { confirmAlert } from "../../../components/modals/ConfirmAlert";
|
||||||
jest.mock("../../../components/modals/ConfirmAlert");
|
jest.mock("../../../components/modals/ConfirmAlert");
|
||||||
|
|
||||||
describe("DeleteUserButton", () => {
|
describe("DeleteUserNavLink", () => {
|
||||||
it("should render nothing, if the delete link is missing", () => {
|
it("should render nothing, if the delete link is missing", () => {
|
||||||
const user = {
|
const user = {
|
||||||
_links: {}
|
_links: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const button = shallow(
|
const navLink = shallow(
|
||||||
<DeleteUserButton user={user} deleteUser={() => {}} />
|
<DeleteUserNavLink user={user} deleteUser={() => {}} />
|
||||||
);
|
);
|
||||||
expect(button.text()).toBe("");
|
expect(navLink.text()).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the button", () => {
|
it("should render the navLink", () => {
|
||||||
const user = {
|
const user = {
|
||||||
_links: {
|
_links: {
|
||||||
delete: {
|
delete: {
|
||||||
@@ -28,13 +28,13 @@ describe("DeleteUserButton", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const button = mount(
|
const navLink = mount(
|
||||||
<DeleteUserButton user={user} deleteUser={() => {}} />
|
<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 = {
|
const user = {
|
||||||
_links: {
|
_links: {
|
||||||
delete: {
|
delete: {
|
||||||
@@ -43,10 +43,10 @@ describe("DeleteUserButton", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const button = mount(
|
const navLink = mount(
|
||||||
<DeleteUserButton user={user} deleteUser={() => {}} />
|
<DeleteUserNavLink user={user} deleteUser={() => {}} />
|
||||||
);
|
);
|
||||||
button.find("a").simulate("click");
|
navLink.find("a").simulate("click");
|
||||||
|
|
||||||
expect(confirmAlert.mock.calls.length).toBe(1);
|
expect(confirmAlert.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -65,14 +65,14 @@ describe("DeleteUserButton", () => {
|
|||||||
calledUrl = user._links.delete.href;
|
calledUrl = user._links.delete.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = mount(
|
const navLink = mount(
|
||||||
<DeleteUserButton
|
<DeleteUserNavLink
|
||||||
user={user}
|
user={user}
|
||||||
confirmDialog={false}
|
confirmDialog={false}
|
||||||
deleteUser={capture}
|
deleteUser={capture}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
button.find("a").simulate("click");
|
navLink.find("a").simulate("click");
|
||||||
|
|
||||||
expect(calledUrl).toBe("/users");
|
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 type { History } from "history";
|
||||||
import { createUser } from "../modules/users";
|
import { createUser } from "../modules/users";
|
||||||
import { Page } from "../../components/layout";
|
import { Page } from "../../components/layout";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
addUser: (user: User, callback?: () => void) => void,
|
addUser: (user: User, callback?: () => void) => void,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
error?: Error,
|
error?: Error,
|
||||||
@@ -25,13 +27,12 @@ class AddUser extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, error } = this.props;
|
const { t, loading, error } = this.props;
|
||||||
|
|
||||||
// TODO i18n
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
title="Create User"
|
title={t("add-user.title")}
|
||||||
subtitle="Create a new user"
|
subtitle={t("add-user.subtitle")}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
>
|
>
|
||||||
@@ -59,4 +60,4 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(AddUser);
|
)(translate("users")(AddUser));
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import { withRouter } from "react-router-dom";
|
import {withRouter} from "react-router-dom";
|
||||||
import UserForm from "./../components/UserForm";
|
import UserForm from "./../components/UserForm";
|
||||||
import type { User } from "../types/User";
|
import type {User} from "../types/User";
|
||||||
import { modifyUser } from "../modules/users";
|
import {modifyUser} from "../modules/users";
|
||||||
import type { History } from "history";
|
import type {History} from "history";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User,
|
user: User,
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import { fetchUser, deleteUser } from "../modules/users";
|
|||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
|
|
||||||
import { Navigation, Section, NavLink } from "../../components/navigation";
|
import { Navigation, Section, NavLink } from "../../components/navigation";
|
||||||
import { DeleteUserButton } from "./../components/buttons";
|
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
||||||
import ErrorPage from "../../components/ErrorPage";
|
import ErrorPage from "../../components/ErrorPage";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
name: string,
|
name: string,
|
||||||
userEntry?: UserEntry,
|
userEntry?: UserEntry,
|
||||||
match: any,
|
match: any,
|
||||||
@@ -49,7 +52,7 @@ class SingleUser extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { userEntry } = this.props;
|
const { t, userEntry } = this.props;
|
||||||
|
|
||||||
if (!userEntry || userEntry.loading) {
|
if (!userEntry || userEntry.loading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
@@ -58,8 +61,8 @@ class SingleUser extends React.Component<Props> {
|
|||||||
if (userEntry.error) {
|
if (userEntry.error) {
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
title="Error"
|
title={t("single-user.error-title")}
|
||||||
subtitle="Unknown user error"
|
subtitle={t("single-user.error-subtitle")}
|
||||||
error={userEntry.error}
|
error={userEntry.error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -68,8 +71,6 @@ class SingleUser extends React.Component<Props> {
|
|||||||
const user = userEntry.entry;
|
const user = userEntry.entry;
|
||||||
const url = this.matchedUrl();
|
const url = this.matchedUrl();
|
||||||
|
|
||||||
// TODO i18n
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page title={user.displayName}>
|
<Page title={user.displayName}>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
@@ -82,13 +83,13 @@ class SingleUser extends React.Component<Props> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Section label="Navigation">
|
<Section label={t("single-user.navigation-label")}>
|
||||||
<NavLink to={`${url}`} label="Information" />
|
<NavLink to={`${url}`} label={t("single-user.information-label")} />
|
||||||
<NavLink to={`${url}/edit`} label="Edit" />
|
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="Actions">
|
<Section label={t("single-user.actions-label")}>
|
||||||
<DeleteUserButton user={user} deleteUser={this.deleteUser} />
|
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
|
||||||
<NavLink to="/users" label="Back" />
|
<NavLink to="/users" label={t("single-user.back-label")} />
|
||||||
</Section>
|
</Section>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,4 +126,4 @@ const mapDispatchToProps = dispatch => {
|
|||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(SingleUser);
|
)(translate("users")(SingleUser));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { apiClient } from "../../apiclient";
|
import { apiClient } from "../../apiclient";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import type { UserEntry } from "../types/UserEntry";
|
import type { UserEntry } from "../types/UserEntry";
|
||||||
import { Dispatch } from "redux";
|
import { combineReducers, Dispatch } from "redux";
|
||||||
import type { Action } from "../../types/Action";
|
import type { Action } from "../../types/Action";
|
||||||
import type { PageCollectionStateSlice } from "../../types/Collection";
|
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_SUCCESS = "scm/users/MODIFY_USER_SUCCESS";
|
||||||
export const MODIFY_USER_FAILURE = "scm/users/MODIFY_USER_FAILURE";
|
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_SUCCESS = "scm/users/DELETE_SUCCESS";
|
||||||
export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE";
|
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";
|
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
||||||
|
|
||||||
|
//TODO i18n
|
||||||
|
|
||||||
//fetch users
|
//fetch users
|
||||||
|
|
||||||
export function fetchUsers() {
|
export function fetchUsers() {
|
||||||
@@ -241,7 +243,7 @@ export function deleteUser(user: User, callback?: () => void) {
|
|||||||
|
|
||||||
export function deleteUserPending(user: User): Action {
|
export function deleteUserPending(user: User): Action {
|
||||||
return {
|
return {
|
||||||
type: DELETE_USER,
|
type: DELETE_USER_PENDING,
|
||||||
payload: user
|
payload: user
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -300,7 +302,8 @@ function extractUsersByNames(
|
|||||||
}
|
}
|
||||||
return usersByNames;
|
return usersByNames;
|
||||||
}
|
}
|
||||||
function deleteUserInUsersByNames(users: {}, userName: any) {
|
|
||||||
|
function deleteUserInUsersByNames(users: {}, userName: string) {
|
||||||
let newUsers = {};
|
let newUsers = {};
|
||||||
for (let username in users) {
|
for (let username in users) {
|
||||||
if (username !== userName) newUsers[username] = users[username];
|
if (username !== userName) newUsers[username] = users[username];
|
||||||
@@ -308,7 +311,7 @@ function deleteUserInUsersByNames(users: {}, userName: any) {
|
|||||||
return newUsers;
|
return newUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteUserInEntries(users: [], userName: any) {
|
function deleteUserInEntries(users: [], userName: string) {
|
||||||
let newUsers = [];
|
let newUsers = [];
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
if (user !== userName) newUsers.push(user);
|
if (user !== userName) newUsers.push(user);
|
||||||
@@ -318,130 +321,87 @@ function deleteUserInEntries(users: [], userName: any) {
|
|||||||
|
|
||||||
const reducerByName = (state: any, username: string, newUserState: any) => {
|
const reducerByName = (state: any, username: string, newUserState: any) => {
|
||||||
const newUsersByNames = {
|
const newUsersByNames = {
|
||||||
...state.byNames,
|
...state,
|
||||||
[username]: newUserState
|
[username]: newUserState
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return newUsersByNames;
|
||||||
...state,
|
|
||||||
byNames: newUsersByNames
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function reducer(state: any = {}, action: any = {}) {
|
function listReducer(state: any = {}, action: any = {}) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
// fetch user list cases
|
// Fetch all users actions
|
||||||
case FETCH_USERS_PENDING:
|
case FETCH_USERS_PENDING:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
list: {
|
loading: true
|
||||||
loading: true
|
};
|
||||||
|
case FETCH_USERS_SUCCESS:
|
||||||
|
const users = action.payload._embedded.users;
|
||||||
|
const userNames = users.map(user => user.name);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
entries: userNames,
|
||||||
|
loading: false,
|
||||||
|
entry: {
|
||||||
|
userCreatePermission: action.payload._links.create ? true : false,
|
||||||
|
page: action.payload.page,
|
||||||
|
pageTotal: action.payload.pageTotal,
|
||||||
|
_links: action.payload._links
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
case FETCH_USERS_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
error: action.payload.error
|
||||||
|
};
|
||||||
|
// 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:
|
case FETCH_USERS_SUCCESS:
|
||||||
const users = action.payload._embedded.users;
|
const users = action.payload._embedded.users;
|
||||||
const userNames = users.map(user => user.name);
|
const userNames = users.map(user => user.name);
|
||||||
const byNames = extractUsersByNames(users, userNames, state.byNames);
|
const byNames = extractUsersByNames(users, userNames, state.byNames);
|
||||||
return {
|
return {
|
||||||
...state,
|
...byNames
|
||||||
list: {
|
|
||||||
error: null,
|
|
||||||
loading: false,
|
|
||||||
entries: userNames,
|
|
||||||
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 {
|
// Fetch single user actions
|
||||||
...state,
|
|
||||||
list: {
|
|
||||||
...state.users,
|
|
||||||
loading: false,
|
|
||||||
error: action.payload.error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Fetch single user cases
|
|
||||||
case FETCH_USER_PENDING:
|
case FETCH_USER_PENDING:
|
||||||
return reducerByName(state, action.payload.name, {
|
return reducerByName(state, action.payload.name, {
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null
|
error: null
|
||||||
});
|
});
|
||||||
|
|
||||||
case FETCH_USER_SUCCESS:
|
case FETCH_USER_SUCCESS:
|
||||||
return reducerByName(state, action.payload.name, {
|
return reducerByName(state, action.payload.name, {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
entry: action.payload
|
entry: action.payload
|
||||||
});
|
});
|
||||||
|
|
||||||
case FETCH_USER_FAILURE:
|
case FETCH_USER_FAILURE:
|
||||||
return reducerByName(state, action.payload.username, {
|
return reducerByName(state, action.payload.username, {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: action.payload.error
|
error: action.payload.error
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete single user cases
|
// Update single user actions
|
||||||
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
|
|
||||||
case MODIFY_USER_PENDING:
|
case MODIFY_USER_PENDING:
|
||||||
return reducerByName(state, action.payload.name, {
|
return reducerByName(state, action.payload.name, {
|
||||||
loading: true
|
loading: true
|
||||||
@@ -454,6 +414,27 @@ export default function reducer(state: any = {}, action: any = {}) {
|
|||||||
return reducerByName(state, action.payload.user.name, {
|
return reducerByName(state, action.payload.user.name, {
|
||||||
error: action.payload.error
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -489,3 +470,31 @@ export const isPermittedToCreateUsers = (state: Object): boolean => {
|
|||||||
}
|
}
|
||||||
return false;
|
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 thunk from "redux-thunk";
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
|
|
||||||
import {
|
import reducer, {
|
||||||
FETCH_USERS_PENDING,
|
CREATE_USER_FAILURE,
|
||||||
FETCH_USERS_SUCCESS,
|
|
||||||
fetchUsers,
|
|
||||||
FETCH_USERS_FAILURE,
|
|
||||||
createUserPending,
|
|
||||||
CREATE_USER_PENDING,
|
CREATE_USER_PENDING,
|
||||||
CREATE_USER_SUCCESS,
|
CREATE_USER_SUCCESS,
|
||||||
CREATE_USER_FAILURE,
|
createUser,
|
||||||
modifyUser,
|
createUserFailure,
|
||||||
MODIFY_USER_PENDING,
|
createUserPending,
|
||||||
MODIFY_USER_FAILURE,
|
createUserSuccess,
|
||||||
MODIFY_USER_SUCCESS,
|
|
||||||
deleteUserPending,
|
|
||||||
deleteUserFailure,
|
|
||||||
DELETE_USER,
|
|
||||||
DELETE_USER_SUCCESS,
|
|
||||||
DELETE_USER_FAILURE,
|
DELETE_USER_FAILURE,
|
||||||
|
DELETE_USER_PENDING,
|
||||||
|
DELETE_USER_SUCCESS,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
fetchUsersFailure,
|
deleteUserFailure,
|
||||||
fetchUsersSuccess,
|
deleteUserPending,
|
||||||
fetchUser,
|
deleteUserSuccess,
|
||||||
|
FETCH_USER_FAILURE,
|
||||||
FETCH_USER_PENDING,
|
FETCH_USER_PENDING,
|
||||||
FETCH_USER_SUCCESS,
|
FETCH_USER_SUCCESS,
|
||||||
FETCH_USER_FAILURE,
|
FETCH_USERS_FAILURE,
|
||||||
createUser,
|
FETCH_USERS_PENDING,
|
||||||
createUserSuccess,
|
FETCH_USERS_SUCCESS,
|
||||||
createUserFailure,
|
fetchUser,
|
||||||
modifyUserPending,
|
|
||||||
modifyUserSuccess,
|
|
||||||
modifyUserFailure,
|
|
||||||
fetchUserSuccess,
|
|
||||||
deleteUserSuccess,
|
|
||||||
fetchUsersPending,
|
|
||||||
fetchUserPending,
|
|
||||||
fetchUserFailure,
|
fetchUserFailure,
|
||||||
|
fetchUserPending,
|
||||||
|
fetchUsers,
|
||||||
|
fetchUsersFailure,
|
||||||
|
fetchUsersPending,
|
||||||
|
fetchUsersSuccess,
|
||||||
|
fetchUserSuccess,
|
||||||
selectListAsCollection,
|
selectListAsCollection,
|
||||||
isPermittedToCreateUsers
|
isPermittedToCreateUsers,
|
||||||
|
MODIFY_USER_FAILURE,
|
||||||
|
MODIFY_USER_PENDING,
|
||||||
|
MODIFY_USER_SUCCESS,
|
||||||
|
modifyUser,
|
||||||
|
modifyUserFailure,
|
||||||
|
modifyUserPending,
|
||||||
|
modifyUserSuccess
|
||||||
} from "./users";
|
} from "./users";
|
||||||
|
|
||||||
import reducer from "./users";
|
|
||||||
|
|
||||||
const userZaphod = {
|
const userZaphod = {
|
||||||
active: true,
|
active: true,
|
||||||
admin: 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 = {
|
const responseBody = {
|
||||||
page: 0,
|
page: 0,
|
||||||
pageTotal: 1,
|
pageTotal: 1,
|
||||||
@@ -309,7 +285,7 @@ describe("users fetch()", () => {
|
|||||||
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions.length).toBe(2);
|
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[0].payload).toBe(userZaphod);
|
||||||
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
|
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
|
||||||
});
|
});
|
||||||
@@ -339,7 +315,7 @@ describe("users fetch()", () => {
|
|||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
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[0].payload).toBe(userZaphod);
|
||||||
expect(actions[1].type).toEqual(DELETE_USER_FAILURE);
|
expect(actions[1].type).toEqual(DELETE_USER_FAILURE);
|
||||||
expect(actions[1].payload).toBeDefined();
|
expect(actions[1].payload).toBeDefined();
|
||||||
@@ -381,205 +357,265 @@ describe("users reducer", () => {
|
|||||||
expect(newState.list.entry.userCreatePermission).toBeTruthy();
|
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 state = {
|
|
||||||
usersByNames: {
|
|
||||||
zaphod: {
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
entry: userZaphod
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const newState = reducer(state, deleteUserPending(userZaphod));
|
|
||||||
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", () => {
|
|
||||||
const state = {
|
|
||||||
usersByNames: {
|
|
||||||
zaphod: {
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
entry: userZaphod
|
|
||||||
},
|
|
||||||
ford: {
|
|
||||||
loading: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const newState = reducer(state, deleteUserPending(userZaphod));
|
|
||||||
const ford = newState.usersByNames["ford"];
|
|
||||||
expect(ford.loading).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set the error of user which could not be deleted", () => {
|
|
||||||
const state = {
|
|
||||||
usersByNames: {
|
|
||||||
zaphod: {
|
|
||||||
loading: true,
|
|
||||||
entry: userZaphod
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = new Error("error");
|
|
||||||
const newState = reducer(state, deleteUserFailure(userZaphod, error));
|
|
||||||
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", () => {
|
|
||||||
const state = {
|
|
||||||
usersByNames: {
|
|
||||||
zaphod: {
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
entry: userZaphod
|
|
||||||
},
|
|
||||||
ford: {
|
|
||||||
loading: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = new Error("error");
|
|
||||||
const newState = reducer(state, deleteUserFailure(userZaphod, error));
|
|
||||||
const ford = newState.usersByNames["ford"];
|
|
||||||
expect(ford.loading).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not replace whole byNames map when fetching users", () => {
|
|
||||||
const oldState = {
|
const oldState = {
|
||||||
byNames: {
|
list: {
|
||||||
ford: {
|
loading: true
|
||||||
entry: userFord
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = reducer(oldState, fetchUsersSuccess(responseBody));
|
const error = new Error("kaputt");
|
||||||
expect(newState.byNames["zaphod"]).toBeDefined();
|
|
||||||
expect(newState.byNames["ford"]).toBeDefined();
|
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 create link is present", () => {
|
it("should set userCreatePermission to true if update link is present", () => {
|
||||||
const newState = reducer({}, fetchUsersSuccess(responseBody));
|
const newState = reducer({}, fetchUsersSuccess(responseBody));
|
||||||
|
|
||||||
expect(newState.list.entry.userCreatePermission).toBeTruthy();
|
expect(newState.list.entry.userCreatePermission).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should update state correctly according to CREATE_USER_PENDING action", () => {
|
it("should not replace whole byNames map when fetching users", () => {
|
||||||
const newState = reducer({}, createUserPending(userZaphod));
|
const oldState = {
|
||||||
expect(newState.create.loading).toBeTruthy();
|
byNames: {
|
||||||
expect(newState.create.error).toBeFalsy();
|
ford: {
|
||||||
});
|
entry: userFord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
it("should update state correctly according to CREATE_USER_SUCCESS action", () => {
|
const newState = reducer(oldState, fetchUsersSuccess(responseBody));
|
||||||
const newState = reducer({ loading: true }, createUserSuccess());
|
expect(newState.byNames["zaphod"]).toBeDefined();
|
||||||
expect(newState.create.loading).toBeFalsy();
|
expect(newState.byNames["ford"]).toBeDefined();
|
||||||
expect(newState.create.error).toBeFalsy();
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should set the loading to false and the error if user could not be created", () => {
|
test("should update state correctly according to DELETE_USER_PENDING action", () => {
|
||||||
const newState = reducer(
|
const state = {
|
||||||
{ loading: true, error: null },
|
byNames: {
|
||||||
createUserFailure(userFord, new Error("kaputt kaputt"))
|
zaphod: {
|
||||||
);
|
loading: false,
|
||||||
expect(newState.create.loading).toBeFalsy();
|
error: null,
|
||||||
expect(newState.create.error).toEqual(new Error("kaputt kaputt"));
|
entry: userZaphod
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
it("should update state according to FETCH_USER_PENDING action", () => {
|
const newState = reducer(state, deleteUserPending(userZaphod));
|
||||||
const newState = reducer({}, fetchUserPending("zaphod"));
|
const zaphod = newState.byNames["zaphod"];
|
||||||
expect(newState.byNames["zaphod"].loading).toBeTruthy();
|
expect(zaphod.loading).toBeTruthy();
|
||||||
});
|
expect(zaphod.entry).toBe(userZaphod);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not affect users state", () => {
|
it("should not effect other users if one user will be deleted", () => {
|
||||||
const newState = reducer(
|
const state = {
|
||||||
{
|
byNames: {
|
||||||
users: {
|
zaphod: {
|
||||||
entries: ["ford"]
|
loading: false,
|
||||||
}
|
error: null,
|
||||||
|
entry: userZaphod
|
||||||
},
|
},
|
||||||
fetchUserPending("zaphod")
|
ford: {
|
||||||
);
|
loading: false
|
||||||
expect(newState.byNames["zaphod"].loading).toBeTruthy();
|
}
|
||||||
expect(newState.users.entries).toEqual(["ford"]);
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
it("should update state according to FETCH_USER_FAILURE action", () => {
|
const newState = reducer(state, deleteUserPending(userZaphod));
|
||||||
const error = new Error("kaputt!");
|
const ford = newState.byNames["ford"];
|
||||||
const newState = reducer({}, fetchUserFailure(userFord.name, error));
|
expect(ford.loading).toBeFalsy();
|
||||||
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 set the error of user which could not be deleted", () => {
|
||||||
const newState = reducer({}, fetchUserSuccess(userFord));
|
const state = {
|
||||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
byNames: {
|
||||||
expect(newState.byNames["ford"].entry).toBe(userFord);
|
zaphod: {
|
||||||
});
|
|
||||||
|
|
||||||
it("should affect users state nor the state of other users", () => {
|
|
||||||
const newState = reducer(
|
|
||||||
{
|
|
||||||
list: {
|
|
||||||
entries: ["zaphod"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchUserSuccess(userFord)
|
|
||||||
);
|
|
||||||
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", () => {
|
|
||||||
const newState = reducer(
|
|
||||||
{
|
|
||||||
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", () => {
|
|
||||||
const newState = reducer(
|
|
||||||
{
|
|
||||||
loading: true,
|
loading: true,
|
||||||
error: new Error("something"),
|
entry: userZaphod
|
||||||
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", () => {
|
const error = new Error("error");
|
||||||
const error = new Error("something went wrong");
|
const newState = reducer(state, deleteUserFailure(userZaphod, error));
|
||||||
const newState = reducer(
|
const zaphod = newState.byNames["zaphod"];
|
||||||
{
|
expect(zaphod.loading).toBeFalsy();
|
||||||
loading: true,
|
expect(zaphod.error).toBe(error);
|
||||||
entry: {}
|
});
|
||||||
|
|
||||||
|
it("should not effect other users if one user could not be deleted", () => {
|
||||||
|
const state = {
|
||||||
|
byNames: {
|
||||||
|
zaphod: {
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
entry: userZaphod
|
||||||
},
|
},
|
||||||
modifyUserFailure(userFord, error)
|
ford: {
|
||||||
);
|
loading: false
|
||||||
expect(newState.byNames["ford"].loading).toBeFalsy();
|
}
|
||||||
expect(newState.byNames["ford"].error).toBe(error);
|
}
|
||||||
expect(newState.byNames["ford"].entry).toBeFalsy();
|
};
|
||||||
});
|
|
||||||
|
const error = new Error("error");
|
||||||
|
const newState = reducer(state, deleteUserFailure(userZaphod, error));
|
||||||
|
const ford = newState.byNames["ford"];
|
||||||
|
expect(ford.loading).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
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(state, deleteUserSuccess(userFord));
|
||||||
|
expect(newState.byNames["zaphod"]).toBeDefined();
|
||||||
|
expect(newState.byNames["ford"]).toBeFalsy();
|
||||||
|
expect(newState.list.entries).toEqual(["zaphod"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
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({ 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", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{ 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", () => {
|
||||||
|
const newState = reducer({}, fetchUserPending("zaphod"));
|
||||||
|
expect(newState.byNames["zaphod"].loading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not affect list state", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{
|
||||||
|
list: {
|
||||||
|
entries: ["ford"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchUserPending("zaphod")
|
||||||
|
);
|
||||||
|
expect(newState.byNames["zaphod"].loading).toBeTruthy();
|
||||||
|
expect(newState.list.entries).toEqual(["ford"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
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", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{
|
||||||
|
list: {
|
||||||
|
entries: ["zaphod"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchUserSuccess(userFord)
|
||||||
|
);
|
||||||
|
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", () => {
|
||||||
|
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", () => {
|
||||||
|
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", () => {
|
||||||
|
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", () => {
|
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;
|
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.google.inject.Inject;
|
||||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
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.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by masuewer on 04.07.18.
|
|
||||||
*/
|
|
||||||
@Path(AuthenticationResource.PATH)
|
@Path(AuthenticationResource.PATH)
|
||||||
public class AuthenticationResource {
|
public class AuthenticationResource {
|
||||||
|
|
||||||
@@ -61,7 +54,7 @@ public class AuthenticationResource {
|
|||||||
public Response authenticateViaForm(
|
public Response authenticateViaForm(
|
||||||
@Context HttpServletRequest request,
|
@Context HttpServletRequest request,
|
||||||
@Context HttpServletResponse response,
|
@Context HttpServletResponse response,
|
||||||
@BeanParam AuthenticationRequest authentication
|
@BeanParam AuthenticationRequestDto authentication
|
||||||
) {
|
) {
|
||||||
return authenticate(request, response, authentication);
|
return authenticate(request, response, authentication);
|
||||||
}
|
}
|
||||||
@@ -78,7 +71,7 @@ public class AuthenticationResource {
|
|||||||
public Response authenticateViaJSONBody(
|
public Response authenticateViaJSONBody(
|
||||||
@Context HttpServletRequest request,
|
@Context HttpServletRequest request,
|
||||||
@Context HttpServletResponse response,
|
@Context HttpServletResponse response,
|
||||||
AuthenticationRequest authentication
|
AuthenticationRequestDto authentication
|
||||||
) {
|
) {
|
||||||
return authenticate(request, response, authentication);
|
return authenticate(request, response, authentication);
|
||||||
}
|
}
|
||||||
@@ -86,7 +79,7 @@ public class AuthenticationResource {
|
|||||||
private Response authenticate(
|
private Response authenticate(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationRequest authentication
|
AuthenticationRequestDto authentication
|
||||||
) {
|
) {
|
||||||
authentication.validate();
|
authentication.validate();
|
||||||
|
|
||||||
@@ -180,51 +173,6 @@ public class AuthenticationResource {
|
|||||||
return Response.noContent().build();
|
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,
|
private Response handleFailedAuthentication(HttpServletRequest request,
|
||||||
AuthenticationException ex, Response.Status status,
|
AuthenticationException ex, Response.Status status,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import de.otto.edison.hal.Links;
|
import de.otto.edison.hal.Links;
|
||||||
import org.mapstruct.AfterMapping;
|
import org.mapstruct.AfterMapping;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
@@ -21,6 +22,11 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
|||||||
@Inject
|
@Inject
|
||||||
private ResourceLinks resourceLinks;
|
private ResourceLinks resourceLinks;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void setResourceLinks(ResourceLinks resourceLinks) {
|
||||||
|
this.resourceLinks = resourceLinks;
|
||||||
|
}
|
||||||
|
|
||||||
@AfterMapping
|
@AfterMapping
|
||||||
void removePassword(@MappingTarget UserDto target) {
|
void removePassword(@MappingTarget UserDto target) {
|
||||||
target.setPassword(UserResource.DUMMY_PASSWORT);
|
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;
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
|
|
||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
// username = "trillian",
|
|
||||||
// password = "secret",
|
|
||||||
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||||
)
|
)
|
||||||
public class MeResourceTest {
|
public class MeResourceTest {
|
||||||
@@ -50,6 +48,8 @@ public class MeResourceTest {
|
|||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||||
|
|
||||||
|
|
||||||
|
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||||
@Mock
|
@Mock
|
||||||
private UriInfo uriInfo;
|
private UriInfo uriInfo;
|
||||||
@Mock
|
@Mock
|
||||||
@@ -67,9 +67,10 @@ public class MeResourceTest {
|
|||||||
public void prepareEnvironment() throws IOException, UserException {
|
public void prepareEnvironment() throws IOException, UserException {
|
||||||
initMocks(this);
|
initMocks(this);
|
||||||
createDummyUser("trillian");
|
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).modify(userCaptor.capture());
|
||||||
doNothing().when(userManager).delete(userCaptor.capture());
|
doNothing().when(userManager).delete(userCaptor.capture());
|
||||||
|
userToDtoMapper.setResourceLinks(resourceLinks);
|
||||||
MeResource meResource = new MeResource(userToDtoMapper, userManager);
|
MeResource meResource = new MeResource(userToDtoMapper, userManager);
|
||||||
dispatcher.getRegistry().addSingletonResource(meResource);
|
dispatcher.getRegistry().addSingletonResource(meResource);
|
||||||
when(uriInfo.getBaseUri()).thenReturn(URI.create("/"));
|
when(uriInfo.getBaseUri()).thenReturn(URI.create("/"));
|
||||||
|
|||||||
Reference in New Issue
Block a user