Merged branches

This commit is contained in:
Philipp Czora
2018-07-27 13:24:28 +02:00
21 changed files with 789 additions and 523 deletions

View File

@@ -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"
} }
} }

View File

@@ -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>
); );
} }

View File

@@ -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} });
}; };
} }

View File

@@ -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);

View File

@@ -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("");
});

View File

@@ -1,2 +0,0 @@
export { default as DeleteUserButton } from "./DeleteUserButton";
export { default as EditUserButton } from "./EditUserButton";

View File

@@ -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);

View File

@@ -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");
}); });

View 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);

View 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("");
});

View File

@@ -0,0 +1,2 @@
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
export { default as EditUserNavLink } from "./EditUserNavLink";

View File

@@ -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));

View File

@@ -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,

View File

@@ -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));

View File

@@ -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
});

View File

@@ -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", () => {

View File

@@ -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");
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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("/"));