Merged 2.0.0-m3

This commit is contained in:
Philipp Czora
2018-11-07 14:14:03 +01:00
29 changed files with 693 additions and 134 deletions

View File

@@ -104,7 +104,7 @@ public final class DiffCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public DiffCommandBuilder retriveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException { public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException {
getDiffResult(outputStream); getDiffResult(outputStream);
return this; return this;

View File

@@ -1,6 +1,7 @@
//@flow //@flow
import React from "react"; import React from "react";
import type { Me } from "@scm-manager/ui-types"; import type { Me } from "@scm-manager/ui-types";
import {Link} from "react-router-dom";
type Props = { type Props = {
me?: Me me?: Me
@@ -15,7 +16,7 @@ class Footer extends React.Component<Props> {
return ( return (
<footer className="footer"> <footer className="footer">
<div className="container is-centered"> <div className="container is-centered">
<p className="has-text-centered">{me.displayName}</p> <p className="has-text-centered"><Link to={"/me"}>{me.displayName}</Link></p>
</div> </div>
</footer> </footer>
); );

View File

@@ -2,5 +2,6 @@
export type Me = { export type Me = {
name: string, name: string,
displayName: string displayName: string,
mail: string
}; };

View File

@@ -38,5 +38,14 @@
"paginator": { "paginator": {
"next": "Next", "next": "Next",
"previous": "Previous" "previous": "Previous"
},
"profile": {
"actions-label": "Actions",
"username": "Username",
"displayName": "Display Name",
"mail": "E-Mail",
"change-password": "Change password",
"error-title": "Error",
"error-subtitle": "Cannot display profile"
} }
} }

View File

@@ -29,6 +29,9 @@
"edit-user-button": { "edit-user-button": {
"label": "Edit" "label": "Edit"
}, },
"set-password-button": {
"label": "Set password"
},
"user-form": { "user-form": {
"submit": "Submit" "submit": "Submit"
}, },
@@ -49,8 +52,11 @@
"name-invalid": "This name is invalid", "name-invalid": "This name is invalid",
"displayname-invalid": "This displayname is invalid", "displayname-invalid": "This displayname is invalid",
"password-invalid": "Password has to be between 6 and 32 characters", "password-invalid": "Password has to be between 6 and 32 characters",
"passwordValidation-invalid": "Passwords have to be the same", "passwordValidation-invalid": "Passwords have to be identical",
"validatePassword": "Please validate password here" "validatePassword": "Confirm password"
},
"password": {
"set-password-successful": "Password successfully set"
}, },
"help": { "help": {
"usernameHelpText": "Unique name of the user.", "usernameHelpText": "Unique name of the user.",

View File

@@ -19,6 +19,7 @@ import SingleGroup from "../groups/containers/SingleGroup";
import AddGroup from "../groups/containers/AddGroup"; import AddGroup from "../groups/containers/AddGroup";
import Config from "../config/containers/Config"; import Config from "../config/containers/Config";
import Profile from "./Profile";
type Props = { type Props = {
authenticated?: boolean authenticated?: boolean
@@ -106,6 +107,12 @@ class Main extends React.Component<Props> {
component={Config} component={Config}
authenticated={authenticated} authenticated={authenticated}
/> />
<ProtectedRoute
exact
path="/me"
component={Profile}
authenticated={authenticated}
/>
</Switch> </Switch>
</div> </div>
); );

View File

@@ -0,0 +1,98 @@
// @flow
import React from "react";
import {
Page,
Navigation,
Section,
MailLink
} from "../../../scm-ui-components/packages/ui-components/src/index";
import { NavLink } from "react-router-dom";
import { getMe } from "../modules/auth";
import { compose } from "redux";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { Me } from "../../../scm-ui-components/packages/ui-types/src/index";
import AvatarWrapper from "../repos/components/changesets/AvatarWrapper";
import { ErrorPage } from "@scm-manager/ui-components";
type Props = {
me: Me,
// Context props
t: string => string
};
type State = {};
class Profile extends React.Component<Props, State> {
render() {
const { me, t } = this.props;
if (me) {
return (
<ErrorPage
title={t("profile.error-title")}
subtitle={t("profile.error-subtitle")}
error={{ name: "Error", message: "'me' is undefined" }}
/>
);
}
return (
<Page title={me.displayName}>
<div className="columns">
<AvatarWrapper>
<div>
<figure className="media-left">
<p className="image is-64x64">
{
// TODO: add avatar
}
</p>
</figure>
</div>
</AvatarWrapper>
<div className="column is-two-quarters">
<table className="table">
<tbody>
<tr>
<td>{t("profile.username")}</td>
<td>{me.name}</td>
</tr>
<tr>
<td>{t("profile.displayName")}</td>
<td>{me.displayName}</td>
</tr>
<tr>
<td>{t("profile.mail")}</td>
<td>
<MailLink address={me.mail} />
</td>
</tr>
</tbody>
</table>
</div>
<div className="column is-one-quarter">
<Navigation>
<Section label={t("profile.actions-label")} />
<NavLink to={"me/password"}>
{t("profile.change-password")}
</NavLink>
</Navigation>
</div>
</div>
</Page>
);
}
}
const mapStateToProps = state => {
return {
me: getMe(state)
};
};
export default compose(
translate("commons"),
connect(mapStateToProps)
)(Profile);

View File

@@ -5,7 +5,6 @@ import GroupForm from "../components/GroupForm";
import { import {
modifyGroup, modifyGroup,
modifyGroupReset, modifyGroupReset,
fetchGroup,
isModifyGroupPending, isModifyGroupPending,
getModifyGroupFailure getModifyGroupFailure
} from "../modules/groups"; } from "../modules/groups";
@@ -29,13 +28,13 @@ class EditGroup extends React.Component<Props> {
const { group, modifyGroupReset } = this.props; const { group, modifyGroupReset } = this.props;
modifyGroupReset(group); modifyGroupReset(group);
} }
groupModified = () => () => {
const { group, history } = this.props; groupModified = (group: Group) => () => {
history.push(`/group/${group.name}`); this.props.history.push(`/group/${group.name}`);
}; };
modifyGroup = (group: Group) => { modifyGroup = (group: Group) => {
this.props.modifyGroup(group, this.groupModified()); this.props.modifyGroup(group, this.groupModified(group));
}; };
render() { render() {

View File

@@ -16,7 +16,7 @@ import type { Group } from "@scm-manager/ui-types";
import type { History } from "history"; import type { History } from "history";
import { import {
deleteGroup, deleteGroup,
fetchGroup, fetchGroupByName,
getGroupByName, getGroupByName,
isFetchGroupPending, isFetchGroupPending,
getFetchGroupFailure, getFetchGroupFailure,
@@ -37,7 +37,7 @@ type Props = {
// dispatcher functions // dispatcher functions
deleteGroup: (group: Group, callback?: () => void) => void, deleteGroup: (group: Group, callback?: () => void) => void,
fetchGroup: (string, string) => void, fetchGroupByName: (string, string) => void,
// context objects // context objects
t: string => string, t: string => string,
@@ -47,7 +47,7 @@ type Props = {
class SingleGroup extends React.Component<Props> { class SingleGroup extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchGroup(this.props.groupLink, this.props.name); this.props.fetchGroupByName(this.props.groupLink, this.props.name);
} }
stripEndingSlash = (url: string) => { stripEndingSlash = (url: string) => {
@@ -147,8 +147,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchGroup: (link: string, name: string) => { fetchGroupByName: (link: string, name: string) => {
dispatch(fetchGroup(link, name)); dispatch(fetchGroupByName(link, name));
}, },
deleteGroup: (group: Group, callback?: () => void) => { deleteGroup: (group: Group, callback?: () => void) => {
dispatch(deleteGroup(group, callback)); dispatch(deleteGroup(group, callback));

View File

@@ -85,12 +85,20 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
} }
//fetch group //fetch group
export function fetchGroup(link: string, name: string) { export function fetchGroupByLink(group: Group) {
return fetchGroup(group._links.self.href, group.name);
}
export function fetchGroupByName(link: string, name: string) {
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name; const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
return fetchGroup(groupUrl, name);
}
function fetchGroup(link: string, name: string) {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchGroupPending(name)); dispatch(fetchGroupPending(name));
return apiClient return apiClient
.get(groupUrl) .get(link)
.then(response => { .then(response => {
return response.json(); return response.json();
}) })
@@ -190,6 +198,9 @@ export function modifyGroup(group: Group, callback?: () => void) {
callback(); callback();
} }
}) })
.then(() => {
dispatch(fetchGroupByLink(group));
})
.catch(cause => { .catch(cause => {
dispatch( dispatch(
modifyGroupFailure( modifyGroupFailure(
@@ -369,8 +380,6 @@ function byNamesReducer(state: any = {}, action: any = {}) {
}; };
case FETCH_GROUP_SUCCESS: case FETCH_GROUP_SUCCESS:
return reducerByName(state, action.payload.name, action.payload); return reducerByName(state, action.payload.name, action.payload);
case MODIFY_GROUP_SUCCESS:
return reducerByName(state, action.payload.name, action.payload);
case DELETE_GROUP_SUCCESS: case DELETE_GROUP_SUCCESS:
const newGroupByNames = deleteGroupInGroupsByNames( const newGroupByNames = deleteGroupInGroupsByNames(
state, state,

View File

@@ -15,7 +15,8 @@ import reducer, {
getFetchGroupsFailure, getFetchGroupsFailure,
isFetchGroupsPending, isFetchGroupsPending,
selectListAsCollection, selectListAsCollection,
fetchGroup, fetchGroupByLink,
fetchGroupByName,
FETCH_GROUP_PENDING, FETCH_GROUP_PENDING,
FETCH_GROUP_SUCCESS, FETCH_GROUP_SUCCESS,
FETCH_GROUP_FAILURE, FETCH_GROUP_FAILURE,
@@ -46,6 +47,7 @@ import reducer, {
getCreateGroupLink getCreateGroupLink
} from "./groups"; } from "./groups";
const GROUPS_URL = "/api/v2/groups"; const GROUPS_URL = "/api/v2/groups";
const URL_HUMAN_GROUP = "http://localhost:8081/api/v2/groups/humanGroup";
const URL = "/groups"; const URL = "/groups";
const error = new Error("You have an error!"); const error = new Error("You have an error!");
@@ -59,13 +61,13 @@ const humanGroup = {
members: ["userZaphod"], members: ["userZaphod"],
_links: { _links: {
self: { self: {
href: "http://localhost:8081/api/v2/groups/humanGroup" href: URL_HUMAN_GROUP
}, },
delete: { delete: {
href: "http://localhost:8081/api/v2/groups/humanGroup" href: URL_HUMAN_GROUP
}, },
update: { update: {
href: "http://localhost:8081/api/v2/groups/humanGroup" href: URL_HUMAN_GROUP
} }
}, },
_embedded: { _embedded: {
@@ -171,11 +173,37 @@ describe("groups fetch()", () => {
}); });
}); });
it("should sucessfully fetch single group", () => { it("should sucessfully fetch single group by name", () => {
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup); fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => { return store.dispatch(fetchGroupByName(URL, "humanGroup")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
expect(actions[1].payload).toBeDefined();
});
});
it("should fail fetching single group by name on HTTP 500", () => {
fetchMock.getOnce(GROUPS_URL + "/humanGroup", {
status: 500
});
const store = mockStore({});
return store.dispatch(fetchGroupByName(URL, "humanGroup")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should sucessfully fetch single group", () => {
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
const store = mockStore({});
return store.dispatch(fetchGroupByLink(humanGroup)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS); expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
@@ -184,12 +212,12 @@ describe("groups fetch()", () => {
}); });
it("should fail fetching single group on HTTP 500", () => { it("should fail fetching single group on HTTP 500", () => {
fetchMock.getOnce(GROUPS_URL + "/humanGroup", { fetchMock.getOnce(URL_HUMAN_GROUP, {
status: 500 status: 500
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => { return store.dispatch(fetchGroupByLink(humanGroup)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE); expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
@@ -244,9 +272,10 @@ describe("groups fetch()", () => {
}); });
it("should successfully modify group", () => { it("should successfully modify group", () => {
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", { fetchMock.putOnce(URL_HUMAN_GROUP, {
status: 204 status: 204
}); });
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
const store = mockStore({}); const store = mockStore({});
@@ -254,14 +283,16 @@ describe("groups fetch()", () => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING); expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS); expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
expect(actions[2].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].payload).toEqual(humanGroup); expect(actions[1].payload).toEqual(humanGroup);
}); });
}); });
it("should call the callback after modifying group", () => { it("should call the callback after modifying group", () => {
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", { fetchMock.putOnce(URL_HUMAN_GROUP, {
status: 204 status: 204
}); });
fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup);
let called = false; let called = false;
const callback = () => { const callback = () => {
@@ -273,12 +304,13 @@ describe("groups fetch()", () => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING); expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS); expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
expect(actions[2].type).toEqual(FETCH_GROUP_PENDING);
expect(called).toBe(true); expect(called).toBe(true);
}); });
}); });
it("should fail modifying group on HTTP 500", () => { it("should fail modifying group on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", { fetchMock.putOnce(URL_HUMAN_GROUP, {
status: 500 status: 500
}); });
@@ -293,7 +325,7 @@ describe("groups fetch()", () => {
}); });
it("should delete successfully group humanGroup", () => { it("should delete successfully group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", { fetchMock.deleteOnce(URL_HUMAN_GROUP, {
status: 204 status: 204
}); });
@@ -308,7 +340,7 @@ describe("groups fetch()", () => {
}); });
it("should call the callback, after successful delete", () => { it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", { fetchMock.deleteOnce(URL_HUMAN_GROUP, {
status: 204 status: 204
}); });
@@ -324,7 +356,7 @@ describe("groups fetch()", () => {
}); });
it("should fail to delete group humanGroup", () => { it("should fail to delete group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", { fetchMock.deleteOnce(URL_HUMAN_GROUP, {
status: 500 status: 500
}); });

View File

@@ -1,5 +1,5 @@
// @flow // @flow
import type { Me } from "@scm-manager/ui-components"; import type { Me } from "@scm-manager/ui-types";
import * as types from "./types"; import * as types from "./types";
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components"; import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
@@ -136,7 +136,11 @@ const callFetchMe = (link: string): Promise<Me> => {
return response.json(); return response.json();
}) })
.then(json => { .then(json => {
return { name: json.name, displayName: json.displayName }; return {
name: json.name,
displayName: json.displayName,
mail: json.mail
};
}); });
}; };

View File

@@ -37,7 +37,11 @@ import {
FETCH_INDEXRESOURCES_SUCCESS FETCH_INDEXRESOURCES_SUCCESS
} from "./indexResource"; } from "./indexResource";
const me = { name: "tricia", displayName: "Tricia McMillian" }; const me = {
name: "tricia",
displayName: "Tricia McMillian",
mail: "trillian@heartofgold.universe"
};
describe("auth reducer", () => { describe("auth reducer", () => {
it("should set me and login on successful fetch of me", () => { it("should set me and login on successful fetch of me", () => {

View File

@@ -1,19 +1,32 @@
//@flow //@flow
import React from "react"; import React from "react";
import {deleteRepo, fetchRepo, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos"; import {
deleteRepo,
fetchRepoByName,
getFetchRepoFailure,
getRepository,
isFetchRepoPending
} from "../modules/repos";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {Route, Switch} from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import type {Repository} from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components"; import {
import {translate} from "react-i18next"; ErrorPage,
Loading,
Navigation,
NavLink,
Page,
Section
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import RepositoryDetails from "../components/RepositoryDetails"; import RepositoryDetails from "../components/RepositoryDetails";
import DeleteNavAction from "../components/DeleteNavAction"; import DeleteNavAction from "../components/DeleteNavAction";
import Edit from "../containers/Edit"; import Edit from "../containers/Edit";
import Permissions from "../permissions/containers/Permissions"; import Permissions from "../permissions/containers/Permissions";
import type {History} from "history"; import type { History } from "history";
import EditNavLink from "../components/EditNavLink"; import EditNavLink from "../components/EditNavLink";
import BranchRoot from "./ChangesetsRoot"; import BranchRoot from "./ChangesetsRoot";
@@ -32,7 +45,7 @@ type Props = {
repoLink: string, repoLink: string,
// dispatch functions // dispatch functions
fetchRepo: (link: string, namespace: string, name: string) => void, fetchRepoByName: (link: string, namespace: string, name: string) => void,
deleteRepo: (repository: Repository, () => void) => void, deleteRepo: (repository: Repository, () => void) => void,
// context props // context props
@@ -43,9 +56,9 @@ type Props = {
class RepositoryRoot extends React.Component<Props> { class RepositoryRoot extends React.Component<Props> {
componentDidMount() { componentDidMount() {
const { fetchRepo, namespace, name, repoLink } = this.props; const { fetchRepoByName, namespace, name, repoLink } = this.props;
fetchRepo(repoLink, namespace, name); fetchRepoByName(repoLink, namespace, name);
} }
stripEndingSlash = (url: string) => { stripEndingSlash = (url: string) => {
@@ -209,8 +222,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchRepo: (link: string, namespace: string, name: string) => { fetchRepoByName: (link: string, namespace: string, name: string) => {
dispatch(fetchRepo(link, namespace, name)); dispatch(fetchRepoByName(link, namespace, name));
}, },
deleteRepo: (repository: Repository, callback: () => void) => { deleteRepo: (repository: Repository, callback: () => void) => {
dispatch(deleteRepo(repository, callback)); dispatch(deleteRepo(repository, callback));

View File

@@ -100,13 +100,20 @@ export function fetchReposFailure(err: Error): Action {
} }
// fetch repo // fetch repo
export function fetchRepoByLink(repo: Repository) {
return fetchRepo(repo._links.self.href, repo.namespace, repo.name);
}
export function fetchRepo(link: string, namespace: string, name: string) { export function fetchRepoByName(link: string, namespace: string, name: string) {
const repoUrl = link.endsWith("/") ? link : link + "/"; const repoUrl = link.endsWith("/") ? link : link + "/";
return fetchRepo(`${repoUrl}${namespace}/${name}`, namespace, name);
}
function fetchRepo(link: string, namespace: string, name: string) {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchRepoPending(namespace, name)); dispatch(fetchRepoPending(namespace, name));
return apiClient return apiClient
.get(`${repoUrl}${namespace}/${name}`) .get(link)
.then(response => response.json()) .then(response => response.json())
.then(repository => { .then(repository => {
dispatch(fetchRepoSuccess(repository)); dispatch(fetchRepoSuccess(repository));
@@ -214,6 +221,9 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
callback(); callback();
} }
}) })
.then(() => {
dispatch(fetchRepoByLink(repository));
})
.catch(cause => { .catch(cause => {
const error = new Error(`failed to modify repo: ${cause.message}`); const error = new Error(`failed to modify repo: ${cause.message}`);
dispatch(modifyRepoFailure(repository, error)); dispatch(modifyRepoFailure(repository, error));
@@ -356,8 +366,6 @@ export default function reducer(
switch (action.type) { switch (action.type) {
case FETCH_REPOS_SUCCESS: case FETCH_REPOS_SUCCESS:
return normalizeByNamespaceAndName(action.payload); return normalizeByNamespaceAndName(action.payload);
case MODIFY_REPO_SUCCESS:
return reducerByNames(state, action.payload);
case FETCH_REPO_SUCCESS: case FETCH_REPO_SUCCESS:
return reducerByNames(state, action.payload); return reducerByNames(state, action.payload);
default: default:

View File

@@ -15,7 +15,8 @@ import reducer, {
fetchReposByLink, fetchReposByLink,
fetchReposByPage, fetchReposByPage,
FETCH_REPO, FETCH_REPO,
fetchRepo, fetchRepoByLink,
fetchRepoByName,
FETCH_REPO_PENDING, FETCH_REPO_PENDING,
FETCH_REPO_SUCCESS, FETCH_REPO_SUCCESS,
FETCH_REPO_FAILURE, FETCH_REPO_FAILURE,
@@ -45,7 +46,6 @@ import reducer, {
MODIFY_REPO, MODIFY_REPO,
isModifyRepoPending, isModifyRepoPending,
getModifyRepoFailure, getModifyRepoFailure,
modifyRepoSuccess,
getPermissionsLink getPermissionsLink
} from "./repos"; } from "./repos";
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types"; import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
@@ -323,7 +323,7 @@ describe("repos fetch", () => {
}); });
}); });
it("should successfully fetch repo slarti/fjords", () => { it("should successfully fetch repo slarti/fjords by name", () => {
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords); fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords);
const expectedActions = [ const expectedActions = [
@@ -343,18 +343,66 @@ describe("repos fetch", () => {
]; ];
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => { return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => { it("should dispatch FETCH_REPO_FAILURE, if the request for slarti/fjords by name fails", () => {
fetchMock.getOnce(REPOS_URL + "/slarti/fjords", { fetchMock.getOnce(REPOS_URL + "/slarti/fjords", {
status: 500 status: 500
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => { return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
expect(actions[1].payload.namespace).toBe("slarti");
expect(actions[1].payload.name).toBe("fjords");
expect(actions[1].payload.error).toBeDefined();
expect(actions[1].itemId).toBe("slarti/fjords");
});
});
it("should successfully fetch repo slarti/fjords", () => {
fetchMock.getOnce(
"http://localhost:8081/api/v2/repositories/slarti/fjords",
slartiFjords
);
const expectedActions = [
{
type: FETCH_REPO_PENDING,
payload: {
namespace: "slarti",
name: "fjords"
},
itemId: "slarti/fjords"
},
{
type: FETCH_REPO_SUCCESS,
payload: slartiFjords,
itemId: "slarti/fjords"
}
];
const store = mockStore({});
return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => {
fetchMock.getOnce(
"http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 500
}
);
const store = mockStore({});
return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_REPO_PENDING); expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE); expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
@@ -485,6 +533,12 @@ describe("repos fetch", () => {
fetchMock.putOnce(slartiFjords._links.update.href, { fetchMock.putOnce(slartiFjords._links.update.href, {
status: 204 status: 204
}); });
fetchMock.getOnce(
"http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 500
}
);
let editedFjords = { ...slartiFjords }; let editedFjords = { ...slartiFjords };
editedFjords.description = "coast of africa"; editedFjords.description = "coast of africa";
@@ -495,6 +549,7 @@ describe("repos fetch", () => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS); expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
expect(actions[2].type).toEqual(FETCH_REPO_PENDING);
}); });
}); });
@@ -502,6 +557,12 @@ describe("repos fetch", () => {
fetchMock.putOnce(slartiFjords._links.update.href, { fetchMock.putOnce(slartiFjords._links.update.href, {
status: 204 status: 204
}); });
fetchMock.getOnce(
"http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 500
}
);
let editedFjords = { ...slartiFjords }; let editedFjords = { ...slartiFjords };
editedFjords.description = "coast of africa"; editedFjords.description = "coast of africa";
@@ -517,6 +578,7 @@ describe("repos fetch", () => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); expect(actions[0].type).toEqual(MODIFY_REPO_PENDING);
expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS); expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS);
expect(actions[2].type).toEqual(FETCH_REPO_PENDING);
expect(called).toBe(true); expect(called).toBe(true);
}); });
}); });
@@ -574,18 +636,6 @@ describe("repos reducer", () => {
const newState = reducer({}, fetchRepoSuccess(slartiFjords)); const newState = reducer({}, fetchRepoSuccess(slartiFjords));
expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords); expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords);
}); });
it("should update reposByNames", () => {
const oldState = {
byNames: {
"slarti/fjords": slartiFjords
}
};
let slartiFjordsEdited = { ...slartiFjords };
slartiFjordsEdited.description = "I bless the rains down in Africa";
const newState = reducer(oldState, modifyRepoSuccess(slartiFjordsEdited));
expect(newState.byNames["slarti/fjords"]).toEqual(slartiFjordsEdited);
});
}); });
describe("repos selectors", () => { describe("repos selectors", () => {

View File

@@ -0,0 +1,177 @@
// @flow
import React from "react";
import type { User } from "@scm-manager/ui-types";
import {
InputField,
SubmitButton,
Notification,
ErrorNotification
} from "@scm-manager/ui-components";
import * as userValidator from "./userValidation";
import { translate } from "react-i18next";
import { updatePassword } from "./updatePassword";
type Props = {
user: User,
t: string => string
};
type State = {
password: string,
loading: boolean,
passwordConfirmationError: boolean,
validatePasswordError: boolean,
validatePassword: string,
error?: Error,
passwordChanged: boolean
};
class SetUserPassword extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
password: "",
loading: false,
passwordConfirmationError: false,
validatePasswordError: false,
validatePassword: "",
passwordChanged: false
};
}
passwordIsValid = () => {
return !(
this.state.validatePasswordError || this.state.passwordConfirmationError
);
};
setLoadingState = () => {
this.setState({
...this.state,
loading: true
});
};
setErrorState = (error: Error) => {
this.setState({
...this.state,
error: error,
loading: false
});
};
setSuccessfulState = () => {
this.setState({
...this.state,
loading: false,
passwordChanged: true,
password: "",
validatePassword: "",
validatePasswordError: false,
passwordConfirmationError: false
});
};
submit = (event: Event) => {
event.preventDefault();
if (this.passwordIsValid()) {
const { user } = this.props;
const { password } = this.state;
this.setLoadingState();
updatePassword(user._links.password.href, password)
.then(result => {
if (result.error) {
this.setErrorState(result.error);
} else {
this.setSuccessfulState();
}
})
.catch(err => {});
}
};
render() {
const { t } = this.props;
const { loading, passwordChanged, error } = this.state;
let message = null;
if (passwordChanged) {
message = (
<Notification
type={"success"}
children={t("password.set-password-successful")}
onClose={() => this.onClose()}
/>
);
} else if (error) {
message = <ErrorNotification error={error} />;
}
return (
<form onSubmit={this.submit}>
{message}
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={this.state.password ? this.state.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordConfirmationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
<SubmitButton
disabled={!this.passwordIsValid()}
loading={loading}
label={t("user-form.submit")}
/>
</form>
);
}
handlePasswordChange = (password: string) => {
const validatePasswordError = !this.checkPasswords(
password,
this.state.validatePassword
);
this.setState({
validatePasswordError: !userValidator.isPasswordValid(password),
passwordConfirmationError: validatePasswordError,
password: password
});
};
handlePasswordValidationChange = (validatePassword: string) => {
const passwordConfirmed = this.checkPasswords(
this.state.password,
validatePassword
);
this.setState({
validatePassword,
passwordConfirmationError: !passwordConfirmed
});
};
checkPasswords = (password1: string, password2: string) => {
return password1 === password2;
};
onClose = () => {
this.setState({
...this.state,
passwordChanged: false
});
};
}
export default translate("users")(SetUserPassword);

View File

@@ -22,7 +22,7 @@ type State = {
mailValidationError: boolean, mailValidationError: boolean,
nameValidationError: boolean, nameValidationError: boolean,
displayNameValidationError: boolean, displayNameValidationError: boolean,
passwordValidationError: boolean, passwordConfirmationError: boolean,
validatePasswordError: boolean, validatePasswordError: boolean,
validatePassword: string validatePassword: string
}; };
@@ -44,7 +44,7 @@ class UserForm extends React.Component<Props, State> {
mailValidationError: false, mailValidationError: false,
displayNameValidationError: false, displayNameValidationError: false,
nameValidationError: false, nameValidationError: false,
passwordValidationError: false, passwordConfirmationError: false,
validatePasswordError: false, validatePasswordError: false,
validatePassword: "" validatePassword: ""
}; };
@@ -70,7 +70,7 @@ class UserForm extends React.Component<Props, State> {
this.state.validatePasswordError || this.state.validatePasswordError ||
this.state.nameValidationError || this.state.nameValidationError ||
this.state.mailValidationError || this.state.mailValidationError ||
this.state.passwordValidationError || this.state.passwordConfirmationError ||
this.state.displayNameValidationError || this.state.displayNameValidationError ||
this.isFalsy(user.name) || this.isFalsy(user.name) ||
this.isFalsy(user.displayName) this.isFalsy(user.displayName)
@@ -89,6 +89,7 @@ class UserForm extends React.Component<Props, State> {
const user = this.state.user; const user = this.state.user;
let nameField = null; let nameField = null;
let passwordFields = null;
if (!this.props.user) { if (!this.props.user) {
nameField = ( nameField = (
<InputField <InputField
@@ -100,6 +101,28 @@ class UserForm extends React.Component<Props, State> {
helpText={t("help.usernameHelpText")} helpText={t("help.usernameHelpText")}
/> />
); );
passwordFields = (
<>
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordConfirmationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
</>
);
} }
return ( return (
<form onSubmit={this.submit}> <form onSubmit={this.submit}>
@@ -120,24 +143,7 @@ class UserForm extends React.Component<Props, State> {
errorMessage={t("validation.mail-invalid")} errorMessage={t("validation.mail-invalid")}
helpText={t("help.mailHelpText")} helpText={t("help.mailHelpText")}
/> />
<InputField {passwordFields}
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<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")}
helpText={t("help.passwordConfirmHelpText")}
/>
<Checkbox <Checkbox
label={t("user.admin")} label={t("user.admin")}
onChange={this.handleAdminChange} onChange={this.handleAdminChange}
@@ -189,7 +195,7 @@ class UserForm extends React.Component<Props, State> {
); );
this.setState({ this.setState({
validatePasswordError: !userValidator.isPasswordValid(password), validatePasswordError: !userValidator.isPasswordValid(password),
passwordValidationError: validatePasswordError, passwordConfirmationError: validatePasswordError,
user: { ...this.state.user, password } user: { ...this.state.user, password }
}); });
}; };
@@ -201,7 +207,7 @@ class UserForm extends React.Component<Props, State> {
); );
this.setState({ this.setState({
validatePassword, validatePassword,
passwordValidationError: !validatePasswordError passwordConfirmationError: !validatePasswordError
}); });
}; };

View File

@@ -0,0 +1,28 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
type Props = {
t: string => string,
user: User,
passwordUrl: String
};
class ChangePasswordNavLink extends React.Component<Props> {
render() {
const { t, passwordUrl } = this.props;
if (!this.hasPermissionToSetPassword()) {
return null;
}
return <NavLink label={t("set-password-button.label")} to={passwordUrl} />;
}
hasPermissionToSetPassword = () => {
return this.props.user._links.password;
};
}
export default translate("users")(ChangePasswordNavLink);

View File

@@ -0,0 +1,31 @@
import React from "react";
import { shallow } from "enzyme";
import "../../../tests/enzyme";
import "../../../tests/i18n";
import ChangePasswordNavLink from "./SetPasswordNavLink";
it("should render nothing, if the password link is missing", () => {
const user = {
_links: {}
};
const navLink = shallow(
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const user = {
_links: {
password: {
href: "/password"
}
}
};
const navLink = shallow(
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
);
expect(navLink.text()).not.toBe("");
});

View File

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

View File

@@ -0,0 +1,15 @@
//@flow
import { apiClient } from "@scm-manager/ui-components";
const CONTENT_TYPE_PASSWORD_OVERWRITE =
"application/vnd.scmm-passwordOverwrite+json;v=2";
export function updatePassword(url: string, password: string) {
return apiClient
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
.then(response => {
return response;
})
.catch(err => {
return { error: err };
});
}

View File

@@ -0,0 +1,23 @@
//@flow
import fetchMock from "fetch-mock";
import { updatePassword } from "./updatePassword";
describe("get content type", () => {
const PASSWORD_URL = "/users/testuser/password";
const password = "testpw123";
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should update password", done => {
fetchMock.put("/api/v2" + PASSWORD_URL, 204);
updatePassword(PASSWORD_URL, password).then(content => {
done();
});
});
});

View File

@@ -15,7 +15,7 @@ import EditUser from "./EditUser";
import type { User } from "@scm-manager/ui-types"; import type { User } from "@scm-manager/ui-types";
import type { History } from "history"; import type { History } from "history";
import { import {
fetchUser, fetchUserByName,
deleteUser, deleteUser,
getUserByName, getUserByName,
isFetchUserPending, isFetchUserPending,
@@ -24,9 +24,14 @@ import {
getDeleteUserFailure getDeleteUserFailure
} from "../modules/users"; } from "../modules/users";
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks"; import {
DeleteUserNavLink,
EditUserNavLink,
SetPasswordNavLink
} from "./../components/navLinks";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource"; import { getUsersLink } from "../../modules/indexResource";
import SetUserPassword from "../components/SetUserPassword";
type Props = { type Props = {
name: string, name: string,
@@ -37,7 +42,7 @@ type Props = {
// dispatcher functions // dispatcher functions
deleteUser: (user: User, callback?: () => void) => void, deleteUser: (user: User, callback?: () => void) => void,
fetchUser: (string, string) => void, fetchUserByName: (string, string) => void,
// context objects // context objects
t: string => string, t: string => string,
@@ -47,7 +52,7 @@ type Props = {
class SingleUser extends React.Component<Props> { class SingleUser extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchUser(this.props.usersLink, this.props.name); this.props.fetchUserByName(this.props.usersLink, this.props.name);
} }
userDeleted = () => { userDeleted = () => {
@@ -97,6 +102,10 @@ class SingleUser extends React.Component<Props> {
path={`${url}/edit`} path={`${url}/edit`}
component={() => <EditUser user={user} />} component={() => <EditUser user={user} />}
/> />
<Route
path={`${url}/password`}
component={() => <SetUserPassword user={user} />}
/>
</div> </div>
<div className="column"> <div className="column">
<Navigation> <Navigation>
@@ -106,6 +115,10 @@ class SingleUser extends React.Component<Props> {
label={t("single-user.information-label")} label={t("single-user.information-label")}
/> />
<EditUserNavLink user={user} editUrl={`${url}/edit`} /> <EditUserNavLink user={user} editUrl={`${url}/edit`} />
<SetPasswordNavLink
user={user}
passwordUrl={`${url}/password`}
/>
</Section> </Section>
<Section label={t("single-user.actions-label")}> <Section label={t("single-user.actions-label")}>
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} /> <DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
@@ -138,8 +151,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchUser: (link: string, name: string) => { fetchUserByName: (link: string, name: string) => {
dispatch(fetchUser(link, name)); dispatch(fetchUserByName(link, name));
}, },
deleteUser: (user: User, callback?: () => void) => { deleteUser: (user: User, callback?: () => void) => {
dispatch(deleteUser(user, callback)); dispatch(deleteUser(user, callback));

View File

@@ -88,12 +88,20 @@ export function fetchUsersFailure(url: string, error: Error): Action {
} }
//fetch user //fetch user
export function fetchUser(link: string, name: string) { export function fetchUserByName(link: string, name: string) {
const userUrl = link.endsWith("/") ? link + name : link + "/" + name; const userUrl = link.endsWith("/") ? link + name : link + "/" + name;
return fetchUser(userUrl, name);
}
export function fetchUserByLink(user: User) {
return fetchUser(user._links.self.href, user.name);
}
function fetchUser(link: string, name: string) {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchUserPending(name)); dispatch(fetchUserPending(name));
return apiClient return apiClient
.get(userUrl) .get(link)
.then(response => { .then(response => {
return response.json(); return response.json();
}) })
@@ -196,6 +204,9 @@ export function modifyUser(user: User, callback?: () => void) {
callback(); callback();
} }
}) })
.then(() => {
dispatch(fetchUserByLink(user));
})
.catch(err => { .catch(err => {
dispatch(modifyUserFailure(user, err)); dispatch(modifyUserFailure(user, err));
}); });
@@ -373,9 +384,6 @@ function byNamesReducer(state: any = {}, action: any = {}) {
case FETCH_USER_SUCCESS: case FETCH_USER_SUCCESS:
return reducerByName(state, action.payload.name, action.payload); return reducerByName(state, action.payload.name, action.payload);
case MODIFY_USER_SUCCESS:
return reducerByName(state, action.payload.name, action.payload);
case DELETE_USER_SUCCESS: case DELETE_USER_SUCCESS:
const newUserByNames = deleteUserInUsersByNames( const newUserByNames = deleteUserInUsersByNames(
state, state,

View File

@@ -20,7 +20,8 @@ import reducer, {
FETCH_USERS_FAILURE, FETCH_USERS_FAILURE,
FETCH_USERS_PENDING, FETCH_USERS_PENDING,
FETCH_USERS_SUCCESS, FETCH_USERS_SUCCESS,
fetchUser, fetchUserByLink,
fetchUserByName,
fetchUserSuccess, fetchUserSuccess,
getFetchUserFailure, getFetchUserFailure,
fetchUsers, fetchUsers,
@@ -33,7 +34,6 @@ import reducer, {
MODIFY_USER_PENDING, MODIFY_USER_PENDING,
MODIFY_USER_SUCCESS, MODIFY_USER_SUCCESS,
modifyUser, modifyUser,
modifyUserSuccess,
getUsersFromState, getUsersFromState,
FETCH_USERS, FETCH_USERS,
getFetchUsersFailure, getFetchUsersFailure,
@@ -124,6 +124,7 @@ const response = {
const URL = "users"; const URL = "users";
const USERS_URL = "/api/v2/users"; const USERS_URL = "/api/v2/users";
const USER_ZAPHOD_URL = "http://localhost:8081/api/v2/users/zaphod";
const error = new Error("KAPUTT"); const error = new Error("KAPUTT");
@@ -166,11 +167,37 @@ describe("users fetch()", () => {
}); });
}); });
it("should sucessfully fetch single user", () => { it("should sucessfully fetch single user by name", () => {
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod); fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchUser(URL, "zaphod")).then(() => { return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
expect(actions[1].payload).toBeDefined();
});
});
it("should fail fetching single user by name on HTTP 500", () => {
fetchMock.getOnce(USERS_URL + "/zaphod", {
status: 500
});
const store = mockStore({});
return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should sucessfully fetch single user", () => {
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
const store = mockStore({});
return store.dispatch(fetchUserByLink(userZaphod)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USER_PENDING); expect(actions[0].type).toEqual(FETCH_USER_PENDING);
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS); expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
@@ -179,12 +206,12 @@ describe("users fetch()", () => {
}); });
it("should fail fetching single user on HTTP 500", () => { it("should fail fetching single user on HTTP 500", () => {
fetchMock.getOnce(USERS_URL + "/zaphod", { fetchMock.getOnce(USER_ZAPHOD_URL, {
status: 500 status: 500
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchUser(URL, "zaphod")).then(() => { return store.dispatch(fetchUserByLink(userZaphod)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_USER_PENDING); expect(actions[0].type).toEqual(FETCH_USER_PENDING);
expect(actions[1].type).toEqual(FETCH_USER_FAILURE); expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
@@ -242,23 +269,26 @@ describe("users fetch()", () => {
}); });
it("successfully update user", () => { it("successfully update user", () => {
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", { fetchMock.putOnce(USER_ZAPHOD_URL, {
status: 204 status: 204
}); });
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
const store = mockStore({}); const store = mockStore({});
return store.dispatch(modifyUser(userZaphod)).then(() => { return store.dispatch(modifyUser(userZaphod)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions.length).toBe(2); expect(actions.length).toBe(3);
expect(actions[0].type).toEqual(MODIFY_USER_PENDING); expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS); expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS);
expect(actions[2].type).toEqual(FETCH_USER_PENDING);
}); });
}); });
it("should call callback, after successful modified user", () => { it("should call callback, after successful modified user", () => {
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", { fetchMock.putOnce(USER_ZAPHOD_URL, {
status: 204 status: 204
}); });
fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod);
let called = false; let called = false;
const callMe = () => { const callMe = () => {
@@ -415,20 +445,6 @@ describe("users reducer", () => {
expect(newState.byNames["ford"]).toBe(userFord); expect(newState.byNames["ford"]).toBe(userFord);
expect(newState.list.entries).toEqual(["zaphod"]); expect(newState.list.entries).toEqual(["zaphod"]);
}); });
it("should update state according to MODIFY_USER_SUCCESS action", () => {
const newState = reducer(
{
byNames: {
ford: {
name: "ford"
}
}
},
modifyUserSuccess(userFord)
);
expect(newState.byNames["ford"]).toBe(userFord);
});
}); });
describe("selector tests", () => { describe("selector tests", () => {

View File

@@ -93,7 +93,7 @@ public class DiffStreamingOutput implements StreamingOutput
public void write(OutputStream output) throws IOException { public void write(OutputStream output) throws IOException {
try try
{ {
builder.retriveContent(output); builder.retrieveContent(output);
} }
catch (RevisionNotFoundException ex) catch (RevisionNotFoundException ex)
{ {

View File

@@ -66,7 +66,7 @@ public class DiffRootResource {
repositoryService.getDiffCommand() repositoryService.getDiffCommand()
.setRevision(revision) .setRevision(revision)
.setFormat(diffFormat) .setFormat(diffFormat)
.retriveContent(output); .retrieveContent(output);
} catch (RevisionNotFoundException e) { } catch (RevisionNotFoundException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND); throw new WebApplicationException(Response.Status.NOT_FOUND);
} }

View File

@@ -92,7 +92,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGetDiffs() throws Exception { public void shouldGetDiffs() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retriveContent(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision") .get(DIFF_URL + "revision")
.accept(VndMediaType.DIFF); .accept(VndMediaType.DIFF);
@@ -124,7 +124,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGet404OnMissingRevision() throws Exception { public void shouldGet404OnMissingRevision() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); when(diffCommandBuilder.retrieveContent(any())).thenThrow(RevisionNotFoundException.class);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision") .get(DIFF_URL + "revision")
@@ -138,7 +138,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGet400OnCrlfInjection() throws Exception { public void shouldGet400OnCrlfInjection() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); when(diffCommandBuilder.retrieveContent(any())).thenThrow(RevisionNotFoundException.class);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634") .get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634")
@@ -152,7 +152,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGet400OnUnknownFormat() throws Exception { public void shouldGet400OnUnknownFormat() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); when(diffCommandBuilder.retrieveContent(any())).thenThrow(RevisionNotFoundException.class);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision?format=Unknown") .get(DIFF_URL + "revision?format=Unknown")
@@ -166,7 +166,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldAcceptDiffFormats() throws Exception { public void shouldAcceptDiffFormats() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retriveContent(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach( Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach(
this::assertRequestOk this::assertRequestOk