diff --git a/scm-ui-components/packages/ui-components/src/forms/Select.js b/scm-ui-components/packages/ui-components/src/forms/Select.js index dd63872b19..880b375999 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Select.js +++ b/scm-ui-components/packages/ui-components/src/forms/Select.js @@ -1,5 +1,6 @@ //@flow import React from "react"; +import classNames from "classnames"; import { LabelWithHelpIcon } from "../index"; export type SelectItem = { @@ -12,6 +13,7 @@ type Props = { options: SelectItem[], value?: SelectItem, onChange: string => void, + loading?: boolean, helpText?: string }; @@ -31,12 +33,17 @@ class Select extends React.Component { }; render() { - const { options, value, label, helpText } = this.props; + const { options, value, label, helpText, loading } = this.props; + const loadingClass = loading ? "is-loading" : ""; + return (
-
+
+ ); + } + + createSelectOptions(types: string[]) { + return types.map(type => { + return { + label: type, + value: type + }; + }); + } +} + +export default translate("permissions")(TypeSelector); diff --git a/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.js b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.js new file mode 100644 index 0000000000..a3ba8616c9 --- /dev/null +++ b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.js @@ -0,0 +1,73 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Permission } from "@scm-manager/ui-types"; +import { confirmAlert, DeleteButton } from "@scm-manager/ui-components"; + +type Props = { + permission: Permission, + namespace: string, + repoName: string, + confirmDialog?: boolean, + t: string => string, + deletePermission: ( + permission: Permission, + namespace: string, + repoName: string + ) => void, + loading: boolean +}; + +class DeletePermissionButton extends React.Component { + static defaultProps = { + confirmDialog: true + }; + + deletePermission = () => { + this.props.deletePermission( + this.props.permission, + this.props.namespace, + this.props.repoName + ); + }; + + confirmDelete = () => { + const { t } = this.props; + confirmAlert({ + title: t("permission.delete-permission-button.confirm-alert.title"), + message: t("permission.delete-permission-button.confirm-alert.message"), + buttons: [ + { + label: t("permission.delete-permission-button.confirm-alert.submit"), + onClick: () => this.deletePermission() + }, + { + label: t("permission.delete-permission-button.confirm-alert.cancel"), + onClick: () => null + } + ] + }); + }; + + isDeletable = () => { + return this.props.permission._links.delete; + }; + + render() { + const { confirmDialog, loading, t } = this.props; + const action = confirmDialog ? this.confirmDelete : this.deletePermission; + + if (!this.isDeletable()) { + return null; + } + return ( + + ); + } +} + +export default translate("repos")(DeletePermissionButton); diff --git a/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.test.js b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.test.js new file mode 100644 index 0000000000..0e722e78e8 --- /dev/null +++ b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.test.js @@ -0,0 +1,91 @@ +import React from "react"; +import { mount, shallow } from "enzyme"; +import "../../../../tests/enzyme"; +import "../../../../tests/i18n"; +import DeletePermissionButton from "./DeletePermissionButton"; + +import { confirmAlert } from "@scm-manager/ui-components"; +jest.mock("@scm-manager/ui-components", () => ({ + confirmAlert: jest.fn(), + DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton +})); + +describe("DeletePermissionButton", () => { + it("should render nothing, if the delete link is missing", () => { + const permission = { + _links: {} + }; + + const navLink = shallow( + {}} + /> + ); + expect(navLink.text()).toBe(""); + }); + + it("should render the navLink", () => { + const permission = { + _links: { + delete: { + href: "/permission" + } + } + }; + + const navLink = mount( + {}} + /> + ); + expect(navLink.text()).not.toBe(""); + }); + + it("should open the confirm dialog on button click", () => { + const permission = { + _links: { + delete: { + href: "/permission" + } + } + }; + + const button = mount( + {}} + /> + ); + button.find("button").simulate("click"); + + expect(confirmAlert.mock.calls.length).toBe(1); + }); + + it("should call the delete permission function with delete url", () => { + const permission = { + _links: { + delete: { + href: "/permission" + } + } + }; + + let calledUrl = null; + function capture(permission) { + calledUrl = permission._links.delete.href; + } + + const button = mount( + + ); + button.find("button").simulate("click"); + + expect(calledUrl).toBe("/permission"); + }); +}); diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.js b/scm-ui/src/repos/permissions/components/permissionValidation.js new file mode 100644 index 0000000000..b74ae40988 --- /dev/null +++ b/scm-ui/src/repos/permissions/components/permissionValidation.js @@ -0,0 +1,23 @@ +// @flow +import { validation } from "@scm-manager/ui-components"; +import type { + PermissionCollection, +} from "@scm-manager/ui-types"; +const isNameValid = validation.isNameValid; + +export { isNameValid }; + +export const isPermissionValid = (name: string, groupPermission: boolean, permissions: PermissionCollection) => { + return isNameValid(name) && !currentPermissionIncludeName(name, groupPermission, permissions); +}; + +const currentPermissionIncludeName = (name: string, groupPermission: boolean, permissions: PermissionCollection) => { + for (let i = 0; i < permissions.length; i++) { + if ( + permissions[i].name === name && + permissions[i].groupPermission == groupPermission + ) + return true; + } + return false; +}; diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.test.js b/scm-ui/src/repos/permissions/components/permissionValidation.test.js new file mode 100644 index 0000000000..036375a348 --- /dev/null +++ b/scm-ui/src/repos/permissions/components/permissionValidation.test.js @@ -0,0 +1,66 @@ +//@flow +import * as validator from "./permissionValidation"; + +describe("permission validation", () => { + it("should return true if permission is valid and does not exist", () => { + const permissions = []; + const name = "PermissionName"; + const groupPermission = false; + + expect( + validator.isPermissionValid(name, groupPermission, permissions) + ).toBe(true); + }); + + it("should return true if permission is valid and does not exists with same group permission", () => { + const permissions = [ + { + name: "PermissionName", + groupPermission: true, + type: "READ" + } + ]; + const name = "PermissionName"; + const groupPermission = false; + + expect( + validator.isPermissionValid(name, groupPermission, permissions) + ).toBe(true); + }); + + it("should return false if permission is valid but exists", () => { + const permissions = [ + { + name: "PermissionName", + groupPermission: false, + type: "READ" + } + ]; + const name = "PermissionName"; + const groupPermission = false; + + expect( + validator.isPermissionValid(name, groupPermission, permissions) + ).toBe(false); + }); + + it("should return false if permission does not exist but is invalid", () => { + const permissions = []; + const name = "@PermissionName"; + const groupPermission = false; + + expect( + validator.isPermissionValid(name, groupPermission, permissions) + ).toBe(false); + }); + + it("should return false if permission is not valid and does not exist", () => { + const permissions = []; + const name = "@PermissionName"; + const groupPermission = false; + + expect( + validator.isPermissionValid(name, groupPermission, permissions) + ).toBe(false); + }); +}); diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js new file mode 100644 index 0000000000..d29359ed0b --- /dev/null +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -0,0 +1,200 @@ +//@flow +import React from "react"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { + fetchPermissions, + getFetchPermissionsFailure, + isFetchPermissionsPending, + getPermissionsOfRepo, + hasCreatePermission, + createPermission, + isCreatePermissionPending, + getCreatePermissionFailure, + createPermissionReset, + getDeletePermissionsFailure, + getModifyPermissionsFailure, + modifyPermissionReset, + deletePermissionReset +} from "../modules/permissions"; +import { Loading, ErrorPage } from "@scm-manager/ui-components"; +import type { + Permission, + PermissionCollection, + PermissionEntry +} from "@scm-manager/ui-types"; +import SinglePermission from "./SinglePermission"; +import CreatePermissionForm from "../components/CreatePermissionForm"; +import type { History } from "history"; + +type Props = { + namespace: string, + repoName: string, + loading: boolean, + error: Error, + permissions: PermissionCollection, + hasPermissionToCreate: boolean, + loadingCreatePermission: boolean, + + //dispatch functions + fetchPermissions: (namespace: string, repoName: string) => void, + createPermission: ( + permission: PermissionEntry, + namespace: string, + repoName: string, + callback?: () => void + ) => void, + createPermissionReset: (string, string) => void, + modifyPermissionReset: (string, string) => void, + deletePermissionReset: (string, string) => void, + // context props + t: string => string, + match: any, + history: History +}; + +class Permissions extends React.Component { + componentDidMount() { + const { + fetchPermissions, + namespace, + repoName, + modifyPermissionReset, + createPermissionReset, + deletePermissionReset + } = this.props; + + createPermissionReset(namespace, repoName); + modifyPermissionReset(namespace, repoName); + deletePermissionReset(namespace, repoName); + fetchPermissions(namespace, repoName); + } + + createPermission = (permission: Permission) => { + this.props.createPermission( + permission, + this.props.namespace, + this.props.repoName + ); + }; + + render() { + const { + loading, + error, + permissions, + t, + namespace, + repoName, + loadingCreatePermission, + hasPermissionToCreate + } = this.props; + if (error) { + return ( + + ); + } + + if (loading || !permissions) { + return ; + } + + const createPermissionForm = hasPermissionToCreate ? ( + this.createPermission(permission)} + loading={loadingCreatePermission} + currentPermissions={permissions} + /> + ) : null; + + return ( +
+ + + + + + + + + + {permissions.map(permission => { + return ( + + ); + })} + +
{t("permission.name")} + {t("permission.group-permission")} + {t("permission.type")}
+ {createPermissionForm} +
+ ); + } +} + +const mapStateToProps = (state, ownProps) => { + const namespace = ownProps.namespace; + const repoName = ownProps.repoName; + const error = + getFetchPermissionsFailure(state, namespace, repoName) || + getCreatePermissionFailure(state, namespace, repoName) || + getDeletePermissionsFailure(state, namespace, repoName) || + getModifyPermissionsFailure(state, namespace, repoName); + const loading = isFetchPermissionsPending(state, namespace, repoName); + const permissions = getPermissionsOfRepo(state, namespace, repoName); + const loadingCreatePermission = isCreatePermissionPending( + state, + namespace, + repoName + ); + const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); + return { + namespace, + repoName, + error, + loading, + permissions, + hasPermissionToCreate, + loadingCreatePermission + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchPermissions: (namespace: string, repoName: string) => { + dispatch(fetchPermissions(namespace, repoName)); + }, + createPermission: ( + permission: PermissionEntry, + namespace: string, + repoName: string, + callback?: () => void + ) => { + dispatch(createPermission(permission, namespace, repoName, callback)); + }, + createPermissionReset: (namespace: string, repoName: string) => { + dispatch(createPermissionReset(namespace, repoName)); + }, + modifyPermissionReset: (namespace: string, repoName: string) => { + dispatch(modifyPermissionReset(namespace, repoName)); + }, + deletePermissionReset: (namespace: string, repoName: string) => { + dispatch(deletePermissionReset(namespace, repoName)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("repos")(Permissions)); diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js new file mode 100644 index 0000000000..9426fbcd9f --- /dev/null +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -0,0 +1,176 @@ +// @flow +import React from "react"; +import type { Permission } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { + modifyPermission, + isModifyPermissionPending, + deletePermission, + isDeletePermissionPending +} from "../modules/permissions"; +import { connect } from "react-redux"; +import type { History } from "history"; +import { Checkbox } from "@scm-manager/ui-components"; +import DeletePermissionButton from "../components/buttons/DeletePermissionButton"; +import TypeSelector from "../components/TypeSelector"; + +type Props = { + submitForm: Permission => void, + modifyPermission: (Permission, string, string) => void, + permission: Permission, + t: string => string, + namespace: string, + repoName: string, + match: any, + history: History, + loading: boolean, + deletePermission: (Permission, string, string) => void, + deleteLoading: boolean +}; + +type State = { + permission: Permission +}; + +class SinglePermission extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + permission: { + name: "", + type: "READ", + groupPermission: false, + _links: {} + } + }; + } + + componentDidMount() { + const { permission } = this.props; + if (permission) { + this.setState({ + permission: { + name: permission.name, + type: permission.type, + groupPermission: permission.groupPermission, + _links: permission._links + } + }); + } + } + + deletePermission = () => { + this.props.deletePermission( + this.props.permission, + this.props.namespace, + this.props.repoName + ); + }; + + render() { + const { permission } = this.state; + const { loading, namespace, repoName } = this.props; + const typeSelector = + this.props.permission._links && this.props.permission._links.update ? ( + + + + ) : ( + {permission.type} + ); + + return ( + + {permission.name} + + + + {typeSelector} + + + + + ); + } + + handleTypeChange = (type: string) => { + this.setState({ + permission: { + ...this.state.permission, + type: type + } + }); + this.modifyPermission(type); + }; + + modifyPermission = (type: string) => { + let permission = this.state.permission; + permission.type = type; + this.props.modifyPermission( + permission, + this.props.namespace, + this.props.repoName + ); + }; + + createSelectOptions(types: string[]) { + return types.map(type => { + return { + label: type, + value: type + }; + }); + } +} + +const mapStateToProps = (state, ownProps) => { + const permission = ownProps.permission; + const loading = isModifyPermissionPending( + state, + ownProps.namespace, + ownProps.repoName, + permission + ); + const deleteLoading = isDeletePermissionPending( + state, + ownProps.namespace, + ownProps.repoName, + permission + ); + + return { loading, deleteLoading }; +}; + +const mapDispatchToProps = dispatch => { + return { + modifyPermission: ( + permission: Permission, + namespace: string, + repoName: string + ) => { + dispatch(modifyPermission(permission, namespace, repoName)); + }, + deletePermission: ( + permission: Permission, + namespace: string, + repoName: string + ) => { + dispatch(deletePermission(permission, namespace, repoName)); + } + }; +}; +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("repos")(SinglePermission)); diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js new file mode 100644 index 0000000000..86d78e7ae9 --- /dev/null +++ b/scm-ui/src/repos/permissions/modules/permissions.js @@ -0,0 +1,624 @@ +// @flow + +import { apiClient } from "@scm-manager/ui-components"; +import * as types from "../../../modules/types"; +import type { Action } from "@scm-manager/ui-components"; +import type { + PermissionCollection, + Permission, + PermissionEntry +} from "@scm-manager/ui-types"; +import { isPending } from "../../../modules/pending"; +import { getFailure } from "../../../modules/failure"; +import { Dispatch } from "redux"; + +export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS"; +export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${ + types.PENDING_SUFFIX +}`; +export const FETCH_PERMISSIONS_SUCCESS = `${FETCH_PERMISSIONS}_${ + types.SUCCESS_SUFFIX +}`; +export const FETCH_PERMISSIONS_FAILURE = `${FETCH_PERMISSIONS}_${ + types.FAILURE_SUFFIX +}`; +export const MODIFY_PERMISSION = "scm/permissions/MODFIY_PERMISSION"; +export const MODIFY_PERMISSION_PENDING = `${MODIFY_PERMISSION}_${ + types.PENDING_SUFFIX +}`; +export const MODIFY_PERMISSION_SUCCESS = `${MODIFY_PERMISSION}_${ + types.SUCCESS_SUFFIX +}`; +export const MODIFY_PERMISSION_FAILURE = `${MODIFY_PERMISSION}_${ + types.FAILURE_SUFFIX +}`; +export const MODIFY_PERMISSION_RESET = `${MODIFY_PERMISSION}_${ + types.RESET_SUFFIX +}`; +export const CREATE_PERMISSION = "scm/permissions/CREATE_PERMISSION"; +export const CREATE_PERMISSION_PENDING = `${CREATE_PERMISSION}_${ + types.PENDING_SUFFIX +}`; +export const CREATE_PERMISSION_SUCCESS = `${CREATE_PERMISSION}_${ + types.SUCCESS_SUFFIX +}`; +export const CREATE_PERMISSION_FAILURE = `${CREATE_PERMISSION}_${ + types.FAILURE_SUFFIX +}`; +export const CREATE_PERMISSION_RESET = `${CREATE_PERMISSION}_${ + types.RESET_SUFFIX +}`; +export const DELETE_PERMISSION = "scm/permissions/DELETE_PERMISSION"; +export const DELETE_PERMISSION_PENDING = `${DELETE_PERMISSION}_${ + types.PENDING_SUFFIX +}`; +export const DELETE_PERMISSION_SUCCESS = `${DELETE_PERMISSION}_${ + types.SUCCESS_SUFFIX +}`; +export const DELETE_PERMISSION_FAILURE = `${DELETE_PERMISSION}_${ + types.FAILURE_SUFFIX +}`; +export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${ + types.RESET_SUFFIX +}`; + +const REPOS_URL = "repositories"; +const PERMISSIONS_URL = "permissions"; +const CONTENT_TYPE = "application/vnd.scmm-permission+json"; + +// fetch permissions + +export function fetchPermissions(namespace: string, repoName: string) { + return function(dispatch: any) { + dispatch(fetchPermissionsPending(namespace, repoName)); + return apiClient + .get(`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`) + .then(response => response.json()) + .then(permissions => { + dispatch(fetchPermissionsSuccess(permissions, namespace, repoName)); + }) + .catch(err => { + dispatch(fetchPermissionsFailure(namespace, repoName, err)); + }); + }; +} + +export function fetchPermissionsPending( + namespace: string, + repoName: string +): Action { + return { + type: FETCH_PERMISSIONS_PENDING, + payload: { + namespace, + repoName + }, + itemId: namespace + "/" + repoName + }; +} + +export function fetchPermissionsSuccess( + permissions: any, + namespace: string, + repoName: string +): Action { + return { + type: FETCH_PERMISSIONS_SUCCESS, + payload: permissions, + itemId: namespace + "/" + repoName + }; +} + +export function fetchPermissionsFailure( + namespace: string, + repoName: string, + error: Error +): Action { + return { + type: FETCH_PERMISSIONS_FAILURE, + payload: { + namespace, + repoName, + error + }, + itemId: namespace + "/" + repoName + }; +} + +// modify permission + +export function modifyPermission( + permission: Permission, + namespace: string, + repoName: string, + callback?: () => void +) { + return function(dispatch: any) { + dispatch(modifyPermissionPending(permission, namespace, repoName)); + return apiClient + .put(permission._links.update.href, permission, CONTENT_TYPE) + .then(() => { + dispatch(modifyPermissionSuccess(permission, namespace, repoName)); + if (callback) { + callback(); + } + }) + .catch(cause => { + const error = new Error( + `failed to modify permission: ${cause.message}` + ); + dispatch( + modifyPermissionFailure(permission, error, namespace, repoName) + ); + }); + }; +} + +export function modifyPermissionPending( + permission: Permission, + namespace: string, + repoName: string +): Action { + return { + type: MODIFY_PERMISSION_PENDING, + payload: permission, + itemId: createItemId(permission, namespace, repoName) + }; +} + +export function modifyPermissionSuccess( + permission: Permission, + namespace: string, + repoName: string +): Action { + return { + type: MODIFY_PERMISSION_SUCCESS, + payload: { + permission, + position: namespace + "/" + repoName + }, + itemId: createItemId(permission, namespace, repoName) + }; +} + +export function modifyPermissionFailure( + permission: Permission, + error: Error, + namespace: string, + repoName: string +): Action { + return { + type: MODIFY_PERMISSION_FAILURE, + payload: { error, permission }, + itemId: createItemId(permission, namespace, repoName) + }; +} + +function newPermissions( + oldPermissions: PermissionCollection, + newPermission: Permission +) { + for (let i = 0; i < oldPermissions.length; i++) { + if (oldPermissions[i].name === newPermission.name) { + oldPermissions.splice(i, 1, newPermission); + return oldPermissions; + } + } +} + +export function modifyPermissionReset(namespace: string, repoName: string) { + return { + type: MODIFY_PERMISSION_RESET, + payload: { + namespace, + repoName + }, + itemId: namespace + "/" + repoName + }; +} + +// create permission +export function createPermission( + permission: PermissionEntry, + namespace: string, + repoName: string, + callback?: () => void +) { + return function(dispatch: Dispatch) { + dispatch(createPermissionPending(permission, namespace, repoName)); + return apiClient + .post( + `${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`, + permission, + CONTENT_TYPE + ) + .then(response => { + const location = response.headers.get("Location"); + return apiClient.get(location); + }) + .then(response => response.json()) + .then(createdPermission => { + dispatch( + createPermissionSuccess(createdPermission, namespace, repoName) + ); + if (callback) { + callback(); + } + }) + .catch(err => + dispatch( + createPermissionFailure( + new Error( + `failed to add permission ${permission.name}: ${err.message}` + ), + namespace, + repoName + ) + ) + ); + }; +} + +export function createPermissionPending( + permission: PermissionEntry, + namespace: string, + repoName: string +): Action { + return { + type: CREATE_PERMISSION_PENDING, + payload: permission, + itemId: namespace + "/" + repoName + }; +} + +export function createPermissionSuccess( + permission: PermissionEntry, + namespace: string, + repoName: string +): Action { + return { + type: CREATE_PERMISSION_SUCCESS, + payload: { + permission, + position: namespace + "/" + repoName + }, + itemId: namespace + "/" + repoName + }; +} + +export function createPermissionFailure( + error: Error, + namespace: string, + repoName: string +): Action { + return { + type: CREATE_PERMISSION_FAILURE, + payload: error, + itemId: namespace + "/" + repoName + }; +} + +export function createPermissionReset(namespace: string, repoName: string) { + return { + type: CREATE_PERMISSION_RESET, + itemId: namespace + "/" + repoName + }; +} + +// delete permission + +export function deletePermission( + permission: Permission, + namespace: string, + repoName: string, + callback?: () => void +) { + return function(dispatch: any) { + dispatch(deletePermissionPending(permission, namespace, repoName)); + return apiClient + .delete(permission._links.delete.href) + .then(() => { + dispatch(deletePermissionSuccess(permission, namespace, repoName)); + if (callback) { + callback(); + } + }) + .catch(cause => { + const error = new Error( + `could not delete permission ${permission.name}: ${cause.message}` + ); + dispatch( + deletePermissionFailure(permission, namespace, repoName, error) + ); + }); + }; +} + +export function deletePermissionPending( + permission: Permission, + namespace: string, + repoName: string +): Action { + return { + type: DELETE_PERMISSION_PENDING, + payload: permission, + itemId: createItemId(permission, namespace, repoName) + }; +} + +export function deletePermissionSuccess( + permission: Permission, + namespace: string, + repoName: string +): Action { + return { + type: DELETE_PERMISSION_SUCCESS, + payload: { + permission, + position: namespace + "/" + repoName + }, + itemId: createItemId(permission, namespace, repoName) + }; +} + +export function deletePermissionFailure( + permission: Permission, + namespace: string, + repoName: string, + error: Error +): Action { + return { + type: DELETE_PERMISSION_FAILURE, + payload: { + error, + permission + }, + itemId: createItemId(permission, namespace, repoName) + }; +} + +export function deletePermissionReset(namespace: string, repoName: string) { + return { + type: DELETE_PERMISSION_RESET, + payload: { + namespace, + repoName + }, + itemId: namespace + "/" + repoName + }; +} +function deletePermissionFromState( + oldPermissions: PermissionCollection, + permission: Permission +) { + let newPermission = []; + for (let i = 0; i < oldPermissions.length; i++) { + if (oldPermissions[i] !== permission) { + newPermission.push(oldPermissions[i]); + } + } + return newPermission; +} + +function createItemId( + permission: Permission, + namespace: string, + repoName: string +) { + let groupPermission = permission.groupPermission ? "@" : ""; + return namespace + "/" + repoName + "/" + groupPermission + permission.name; +} + +// reducer +export default function reducer( + state: Object = {}, + action: Action = { type: "UNKNOWN" } +): Object { + if (!action.payload) { + return state; + } + switch (action.type) { + case FETCH_PERMISSIONS_SUCCESS: + return { + ...state, + [action.itemId]: { + entries: action.payload._embedded.permissions, + createPermission: action.payload._links.create ? true : false + } + }; + case MODIFY_PERMISSION_SUCCESS: + const positionOfPermission = action.payload.position; + const newPermission = newPermissions( + state[action.payload.position].entries, + action.payload.permission + ); + return { + ...state, + [positionOfPermission]: { + ...state[positionOfPermission], + entries: newPermission + } + }; + case CREATE_PERMISSION_SUCCESS: + // return state; + const position = action.payload.position; + const permissions = state[action.payload.position].entries; + permissions.push(action.payload.permission); + return { + ...state, + [position]: { + ...state[position], + entries: permissions + } + }; + case DELETE_PERMISSION_SUCCESS: + const permissionPosition = action.payload.position; + const new_Permissions = deletePermissionFromState( + state[action.payload.position].entries, + action.payload.permission + ); + return { + ...state, + [permissionPosition]: { + ...state[permissionPosition], + entries: new_Permissions + } + }; + default: + return state; + } +} + +// selectors + +export function getPermissionsOfRepo( + state: Object, + namespace: string, + repoName: string +) { + if (state.permissions && state.permissions[namespace + "/" + repoName]) { + const permissions = state.permissions[namespace + "/" + repoName].entries; + return permissions; + } +} + +export function isFetchPermissionsPending( + state: Object, + namespace: string, + repoName: string +) { + return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName); +} + +export function getFetchPermissionsFailure( + state: Object, + namespace: string, + repoName: string +) { + return getFailure(state, FETCH_PERMISSIONS, namespace + "/" + repoName); +} + +export function isModifyPermissionPending( + state: Object, + namespace: string, + repoName: string, + permission: Permission +) { + return isPending( + state, + MODIFY_PERMISSION, + createItemId(permission, namespace, repoName) + ); +} + +export function getModifyPermissionFailure( + state: Object, + namespace: string, + repoName: string, + permission: Permission +) { + return getFailure( + state, + MODIFY_PERMISSION, + createItemId(permission, namespace, repoName) + ); +} + +export function hasCreatePermission( + state: Object, + namespace: string, + repoName: string +) { + if (state.permissions && state.permissions[namespace + "/" + repoName]) + return state.permissions[namespace + "/" + repoName].createPermission; + else return null; +} + +export function isCreatePermissionPending( + state: Object, + namespace: string, + repoName: string +) { + return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName); +} +export function getCreatePermissionFailure( + state: Object, + namespace: string, + repoName: string +) { + return getFailure(state, CREATE_PERMISSION, namespace + "/" + repoName); +} + +export function isDeletePermissionPending( + state: Object, + namespace: string, + repoName: string, + permission: Permission +) { + return isPending( + state, + DELETE_PERMISSION, + createItemId(permission, namespace, repoName) + ); +} + +export function getDeletePermissionFailure( + state: Object, + namespace: string, + repoName: string, + permission: Permission +) { + return getFailure( + state, + DELETE_PERMISSION, + createItemId(permission, namespace, repoName) + ); +} + +export function getDeletePermissionsFailure( + state: Object, + namespace: string, + repoName: string +) { + const permissions = + state.permissions && state.permissions[namespace + "/" + repoName] + ? state.permissions[namespace + "/" + repoName].entries + : null; + if (permissions == null) return undefined; + for (let i = 0; i < permissions.length; i++) { + if ( + getDeletePermissionFailure(state, namespace, repoName, permissions[i]) + ) { + return getFailure( + state, + DELETE_PERMISSION, + createItemId(permissions[i], namespace, repoName) + ); + } + } + return null; +} + +export function getModifyPermissionsFailure( + state: Object, + namespace: string, + repoName: string +) { + const permissions = + state.permissions && state.permissions[namespace + "/" + repoName] + ? state.permissions[namespace + "/" + repoName].entries + : null; + if (permissions == null) return undefined; + for (let i = 0; i < permissions.length; i++) { + if ( + getModifyPermissionFailure(state, namespace, repoName, permissions[i]) + ) { + return getFailure( + state, + MODIFY_PERMISSION, + createItemId(permissions[i], namespace, repoName) + ); + } + } + return null; +} diff --git a/scm-ui/src/repos/permissions/modules/permissions.test.js b/scm-ui/src/repos/permissions/modules/permissions.test.js new file mode 100644 index 0000000000..e546f6cb00 --- /dev/null +++ b/scm-ui/src/repos/permissions/modules/permissions.test.js @@ -0,0 +1,769 @@ +// @flow +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; +import reducer, { + fetchPermissions, + fetchPermissionsSuccess, + getPermissionsOfRepo, + isFetchPermissionsPending, + getFetchPermissionsFailure, + modifyPermission, + modifyPermissionSuccess, + getModifyPermissionFailure, + isModifyPermissionPending, + createPermission, + hasCreatePermission, + deletePermission, + deletePermissionSuccess, + getDeletePermissionFailure, + isDeletePermissionPending, + getModifyPermissionsFailure, + MODIFY_PERMISSION_FAILURE, + MODIFY_PERMISSION_PENDING, + FETCH_PERMISSIONS, + FETCH_PERMISSIONS_PENDING, + FETCH_PERMISSIONS_SUCCESS, + FETCH_PERMISSIONS_FAILURE, + MODIFY_PERMISSION_SUCCESS, + MODIFY_PERMISSION, + CREATE_PERMISSION_PENDING, + CREATE_PERMISSION_SUCCESS, + CREATE_PERMISSION_FAILURE, + DELETE_PERMISSION, + DELETE_PERMISSION_PENDING, + DELETE_PERMISSION_SUCCESS, + DELETE_PERMISSION_FAILURE, + CREATE_PERMISSION, + createPermissionSuccess, + getCreatePermissionFailure, + isCreatePermissionPending, + getDeletePermissionsFailure +} from "./permissions"; +import type { Permission, PermissionCollection } from "@scm-manager/ui-types"; + +const hitchhiker_puzzle42Permission_user_eins: Permission = { + name: "user_eins", + type: "READ", + groupPermission: false, + _links: { + self: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins" + }, + delete: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins" + }, + update: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins" + } + } +}; + +const hitchhiker_puzzle42Permission_user_zwei: Permission = { + name: "user_zwei", + type: "WRITE", + groupPermission: true, + _links: { + self: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei" + }, + delete: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei" + }, + update: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei" + } + } +}; + +const hitchhiker_puzzle42Permissions: PermissionCollection = [ + hitchhiker_puzzle42Permission_user_eins, + hitchhiker_puzzle42Permission_user_zwei +]; + +const hitchhiker_puzzle42RepoPermissions = { + _embedded: { + permissions: hitchhiker_puzzle42Permissions + }, + _links: { + create: { + href: + "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions" + } + } +}; + +describe("permission fetch", () => { + const REPOS_URL = "/api/v2/repositories"; + const mockStore = configureMockStore([thunk]); + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should successfully fetch permissions to repo hitchhiker/puzzle42", () => { + fetchMock.getOnce( + REPOS_URL + "/hitchhiker/puzzle42/permissions", + hitchhiker_puzzle42RepoPermissions + ); + + const expectedActions = [ + { + type: FETCH_PERMISSIONS_PENDING, + payload: { + namespace: "hitchhiker", + repoName: "puzzle42" + }, + itemId: "hitchhiker/puzzle42" + }, + { + type: FETCH_PERMISSIONS_SUCCESS, + payload: hitchhiker_puzzle42RepoPermissions, + itemId: "hitchhiker/puzzle42" + } + ]; + + const store = mockStore({}); + return store + .dispatch(fetchPermissions("hitchhiker", "puzzle42")) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should dispatch FETCH_PERMISSIONS_FAILURE, it the request fails", () => { + fetchMock.getOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", { + status: 500 + }); + + const store = mockStore({}); + return store + .dispatch(fetchPermissions("hitchhiker", "puzzle42")) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING); + expect(actions[1].type).toEqual(FETCH_PERMISSIONS_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should successfully modify user_eins permission", () => { + fetchMock.putOnce( + hitchhiker_puzzle42Permission_user_eins._links.update.href, + { + status: 204 + } + ); + + let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins }; + editedPermission.type = "OWNER"; + + const store = mockStore({}); + + return store + .dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42")) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING); + expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS); + }); + }); + + it("should successfully modify user_eins permission and call the callback", () => { + fetchMock.putOnce( + hitchhiker_puzzle42Permission_user_eins._links.update.href, + { + status: 204 + } + ); + + let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins }; + editedPermission.type = "OWNER"; + + const store = mockStore({}); + + let called = false; + const callback = () => { + called = true; + }; + + return store + .dispatch( + modifyPermission(editedPermission, "hitchhiker", "puzzle42", callback) + ) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING); + expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS); + expect(called).toBe(true); + }); + }); + + it("should fail modifying on HTTP 500", () => { + fetchMock.putOnce( + hitchhiker_puzzle42Permission_user_eins._links.update.href, + { + status: 500 + } + ); + + let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins }; + editedPermission.type = "OWNER"; + + const store = mockStore({}); + + return store + .dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42")) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING); + expect(actions[1].type).toEqual(MODIFY_PERMISSION_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should add a permission successfully", () => { + // unmatched + fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", { + status: 204, + headers: { + location: "repositories/hitchhiker/puzzle42/permissions/user_eins" + } + }); + + fetchMock.getOnce( + REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins", + hitchhiker_puzzle42Permission_user_eins + ); + + const store = mockStore({}); + return store + .dispatch( + createPermission( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42" + ) + ) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING); + expect(actions[1].type).toEqual(CREATE_PERMISSION_SUCCESS); + }); + }); + + it("should fail adding a permission on HTTP 500", () => { + fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", { + status: 500 + }); + + const store = mockStore({}); + return store + .dispatch( + createPermission( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42" + ) + ) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING); + expect(actions[1].type).toEqual(CREATE_PERMISSION_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should call the callback after permission successfully created", () => { + // unmatched + fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", { + status: 204, + headers: { + location: "repositories/hitchhiker/puzzle42/permissions/user_eins" + } + }); + + fetchMock.getOnce( + REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins", + hitchhiker_puzzle42Permission_user_eins + ); + let callMe = "not yet"; + + const callback = () => { + callMe = "yeah"; + }; + + const store = mockStore({}); + return store + .dispatch( + createPermission( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42", + callback + ) + ) + .then(() => { + expect(callMe).toBe("yeah"); + }); + }); + it("should delete successfully permission user_eins", () => { + fetchMock.deleteOnce( + hitchhiker_puzzle42Permission_user_eins._links.delete.href, + { + status: 204 + } + ); + + const store = mockStore({}); + return store + .dispatch( + deletePermission( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42" + ) + ) + .then(() => { + const actions = store.getActions(); + expect(actions.length).toBe(2); + expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING); + expect(actions[0].payload).toBe( + hitchhiker_puzzle42Permission_user_eins + ); + expect(actions[1].type).toEqual(DELETE_PERMISSION_SUCCESS); + }); + }); + + it("should call the callback, after successful delete", () => { + fetchMock.deleteOnce( + hitchhiker_puzzle42Permission_user_eins._links.delete.href, + { + status: 204 + } + ); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store + .dispatch( + deletePermission( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42", + callMe + ) + ) + .then(() => { + expect(called).toBeTruthy(); + }); + }); + + it("should fail to delete permission", () => { + fetchMock.deleteOnce( + hitchhiker_puzzle42Permission_user_eins._links.delete.href, + { + status: 500 + } + ); + + const store = mockStore({}); + return store + .dispatch( + deletePermission( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42" + ) + ) + .then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING); + expect(actions[0].payload).toBe( + hitchhiker_puzzle42Permission_user_eins + ); + expect(actions[1].type).toEqual(DELETE_PERMISSION_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); +}); + +describe("permissions reducer", () => { + it("should return empty object, if state and action is undefined", () => { + expect(reducer()).toEqual({}); + }); + + it("should return the same state, if the action is undefined", () => { + const state = { x: true }; + expect(reducer(state)).toBe(state); + }); + + it("should return the same state, if the action is unknown to the reducer", () => { + const state = { x: true }; + expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state); + }); + + it("should store the permissions on FETCH_PERMISSION_SUCCESS", () => { + const newState = reducer( + {}, + fetchPermissionsSuccess( + hitchhiker_puzzle42RepoPermissions, + "hitchhiker", + "puzzle42" + ) + ); + + expect(newState["hitchhiker/puzzle42"].entries).toBe( + hitchhiker_puzzle42Permissions + ); + }); + + it("should update permission", () => { + const oldState = { + "hitchhiker/puzzle42": { + entries: [hitchhiker_puzzle42Permission_user_eins] + } + }; + let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins }; + permissionEdited.type = "OWNER"; + let expectedState = { + "hitchhiker/puzzle42": { + entries: [permissionEdited] + } + }; + const newState = reducer( + oldState, + modifyPermissionSuccess(permissionEdited, "hitchhiker", "puzzle42") + ); + expect(newState["hitchhiker/puzzle42"]).toEqual( + expectedState["hitchhiker/puzzle42"] + ); + }); + + it("should remove permission from state when delete succeeds", () => { + const state = { + "hitchhiker/puzzle42": { + entries: [ + hitchhiker_puzzle42Permission_user_eins, + hitchhiker_puzzle42Permission_user_zwei + ] + } + }; + + const expectedState = { + "hitchhiker/puzzle42": { + entries: [hitchhiker_puzzle42Permission_user_zwei] + } + }; + + const newState = reducer( + state, + deletePermissionSuccess( + hitchhiker_puzzle42Permission_user_eins, + "hitchhiker", + "puzzle42" + ) + ); + expect(newState["hitchhiker/puzzle42"]).toEqual( + expectedState["hitchhiker/puzzle42"] + ); + }); + + it("should add permission", () => { + //changing state had to be removed because of errors + const oldState = { + "hitchhiker/puzzle42": { + entries: [hitchhiker_puzzle42Permission_user_eins] + } + }; + let expectedState = { + "hitchhiker/puzzle42": { + entries: [ + hitchhiker_puzzle42Permission_user_eins, + hitchhiker_puzzle42Permission_user_zwei + ] + } + }; + const newState = reducer( + oldState, + createPermissionSuccess( + hitchhiker_puzzle42Permission_user_zwei, + "hitchhiker", + "puzzle42" + ) + ); + expect(newState["hitchhiker/puzzle42"]).toEqual( + expectedState["hitchhiker/puzzle42"] + ); + }); +}); + +describe("permissions selectors", () => { + const error = new Error("something goes wrong"); + + it("should return the permissions of one repository", () => { + const state = { + permissions: { + "hitchhiker/puzzle42": { + entries: hitchhiker_puzzle42Permissions + } + } + }; + + const repoPermissions = getPermissionsOfRepo( + state, + "hitchhiker", + "puzzle42" + ); + expect(repoPermissions).toEqual(hitchhiker_puzzle42Permissions); + }); + + it("should return true, when fetch permissions is pending", () => { + const state = { + pending: { + [FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: true + } + }; + expect(isFetchPermissionsPending(state, "hitchhiker", "puzzle42")).toEqual( + true + ); + }); + + it("should return false, when fetch permissions is not pending", () => { + expect(isFetchPermissionsPending({}, "hitchiker", "puzzle42")).toEqual( + false + ); + }); + + it("should return error when fetch permissions did fail", () => { + const state = { + failure: { + [FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: error + } + }; + expect(getFetchPermissionsFailure(state, "hitchhiker", "puzzle42")).toEqual( + error + ); + }); + + it("should return undefined when fetch permissions did not fail", () => { + expect(getFetchPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe( + undefined + ); + }); + + it("should return true, when modify permission is pending", () => { + const state = { + pending: { + [MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true + } + }; + expect( + isModifyPermissionPending( + state, + "hitchhiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toEqual(true); + }); + + it("should return false, when modify permission is not pending", () => { + expect( + isModifyPermissionPending( + {}, + "hitchiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toEqual(false); + }); + + it("should return error when modify permission did fail", () => { + const state = { + failure: { + [MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error + } + }; + expect( + getModifyPermissionFailure( + state, + "hitchhiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toEqual(error); + }); + + it("should return undefined when modify permission did not fail", () => { + expect( + getModifyPermissionFailure( + {}, + "hitchhiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toBe(undefined); + }); + + it("should return error when one of the modify permissions did fail", () => { + const state = { + permissions: { + "hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions } + }, + failure: { + [MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error + } + }; + expect( + getModifyPermissionsFailure(state, "hitchhiker", "puzzle42") + ).toEqual(error); + }); + + it("should return undefined when no modify permissions did not fail", () => { + expect(getModifyPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe( + undefined + ); + }); + + it("should return true, when createPermission is true", () => { + const state = { + permissions: { + ["hitchhiker/puzzle42"]: { + createPermission: true + } + } + }; + expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toBe(true); + }); + + it("should return false, when createPermission is false", () => { + const state = { + permissions: { + ["hitchhiker/puzzle42"]: { + createPermission: false + } + } + }; + expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toEqual(false); + }); + + it("should return true, when delete permission is pending", () => { + const state = { + pending: { + [DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true + } + }; + expect( + isDeletePermissionPending( + state, + "hitchhiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toEqual(true); + }); + + it("should return false, when delete permission is not pending", () => { + expect( + isDeletePermissionPending( + {}, + "hitchiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toEqual(false); + }); + + it("should return error when delete permission did fail", () => { + const state = { + failure: { + [DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error + } + }; + expect( + getDeletePermissionFailure( + state, + "hitchhiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toEqual(error); + }); + + it("should return undefined when delete permission did not fail", () => { + expect( + getDeletePermissionFailure( + {}, + "hitchhiker", + "puzzle42", + hitchhiker_puzzle42Permission_user_eins + ) + ).toBe(undefined); + }); + + it("should return error when one of the delete permissions did fail", () => { + const state = { + permissions: { + "hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions } + }, + failure: { + [DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error + } + }; + expect( + getDeletePermissionsFailure(state, "hitchhiker", "puzzle42") + ).toEqual(error); + }); + + it("should return undefined when no delete permissions did not fail", () => { + expect(getDeletePermissionsFailure({}, "hitchhiker", "puzzle42")).toBe( + undefined + ); + }); + + it("should return true, when create permission is pending", () => { + const state = { + pending: { + [CREATE_PERMISSION + "/hitchhiker/puzzle42"]: true + } + }; + expect(isCreatePermissionPending(state, "hitchhiker", "puzzle42")).toEqual( + true + ); + }); + + it("should return false, when create permissions is not pending", () => { + expect(isCreatePermissionPending({}, "hitchiker", "puzzle42")).toEqual( + false + ); + }); + + it("should return error when create permissions did fail", () => { + const state = { + failure: { + [CREATE_PERMISSION + "/hitchhiker/puzzle42"]: error + } + }; + expect(getCreatePermissionFailure(state, "hitchhiker", "puzzle42")).toEqual( + error + ); + }); + + it("should return undefined when create permissions did not fail", () => { + expect(getCreatePermissionFailure({}, "hitchhiker", "puzzle42")).toBe( + undefined + ); + }); +}); diff --git a/scm-ui/src/users/components/userValidation.js b/scm-ui/src/users/components/userValidation.js index ca66e22405..c9460fdd50 100644 --- a/scm-ui/src/users/components/userValidation.js +++ b/scm-ui/src/users/components/userValidation.js @@ -13,5 +13,5 @@ export const isDisplayNameValid = (displayName: string) => { return false; }; export const isPasswordValid = (password: string) => { - return password.length > 6 && password.length < 32; + return password.length >= 6 && password.length < 32; }; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index cbb3fe0e46..af1ba3bf64 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -78,7 +78,8 @@ public class PermissionRootResource { checkPermissionAlreadyExists(permission, repository); repository.getPermissions().add(dtoToModelMapper.map(permission)); manager.modify(repository); - return Response.created(URI.create(resourceLinks.permission().self(namespace, name, permission.getName()))).build(); + String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission); + return Response.created(URI.create(resourceLinks.permission().self(namespace, name, urlPermissionName))).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java index 8ebe10eb6f..d6ab3721cf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -41,9 +41,7 @@ public abstract class PermissionToPermissionDtoMapper { */ @AfterMapping void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) { - String permissionName = Optional.of(target.getName()) - .filter(p -> !target.isGroupPermission()) - .orElse(GROUP_PREFIX + target.getName()); + String permissionName = getUrlPermissionName(target); Links.Builder linksBuilder = linkingTo() .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName)); if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { @@ -52,4 +50,10 @@ public abstract class PermissionToPermissionDtoMapper { } target.add(linksBuilder.build()); } + + public String getUrlPermissionName(PermissionDto permissionDto) { + return Optional.of(permissionDto.getName()) + .filter(p -> !permissionDto.isGroupPermission()) + .orElse(GROUP_PREFIX + permissionDto.getName()); + } }