Skip to content
Snippets Groups Projects
Commit ab22242a authored by Tom Robinson's avatar Tom Robinson
Browse files

Fix various aspects of admin people modals, add some tests

parent b614b3da
No related branches found
No related tags found
No related merge requests found
Showing
with 430 additions and 235 deletions
import React from "react";
import { compose } from "redux";
import { connect } from "react-redux";
import { goBack } from "react-router-redux";
import { t } from "c-3po";
import Users from "metabase/entities/users";
import User from "metabase/entities/users";
import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader";
import UserForm from "metabase/admin/people/containers/UserForm";
import ModalContent from "metabase/components/ModalContent";
@entityObjectLoader({
entityType: "users",
entityId: (state, props) => props.params.userId,
})
@connect(null, { goBack })
class EditUserModal extends React.Component {
render() {
const { object, goBack } = this.props;
return <UserForm user={object} onClose={goBack} onSaved={goBack} />;
}
}
const EditUserModal = ({ user, onClose, ...props }) => (
<ModalContent title={t`Edit user`} onClose={onClose}>
<User.Form user={user} {...props} onSaved={onClose} />
</ModalContent>
);
export default EditUserModal;
export default compose(
User.loader({ id: (state, props) => props.params.userId }),
connect(null, { onClose: goBack }),
)(EditUserModal);
import React, { Component } from "react";
import { connect } from "react-redux";
import { getGroup, getGroups, getUsers } from "../selectors";
import { getGroup, getGroups, getUsersWithMemberships } from "../selectors";
import { loadGroups, loadGroupDetails } from "../people";
import GroupDetail from "../components/GroupDetail.jsx";
......@@ -10,7 +10,7 @@ function mapStateToProps(state, props) {
return {
group: getGroup(state, props),
groups: getGroups(state, props),
users: getUsers(state, props),
users: getUsersWithMemberships(state, props),
};
}
......
import React from "react";
import * as Urls from "metabase/lib/urls";
import { connect } from "react-redux";
import { goBack, push } from "react-router-redux";
import { t } from "c-3po";
import * as Urls from "metabase/lib/urls";
import User from "metabase/entities/users";
import UserForm from "metabase/admin/people/containers/UserForm";
import ModalContent from "metabase/components/ModalContent";
const NewUserModal = ({ onClose, goBack, push }) => (
<UserForm
user={{}}
onClose={goBack}
onSaved={({ id, password }) => push(Urls.newUserSuccess(id, password))}
/>
const NewUserModal = ({ onClose, onSaved, ...props }) => (
<ModalContent title={t`New user`} onClose={onClose}>
<User.Form {...props} onSaved={onSaved} />
</ModalContent>
);
export default connect(null, { goBack, push })(NewUserModal);
export default connect(null, {
onClose: goBack,
onSaved: user => push(Urls.newUserSuccess(user.id)),
})(NewUserModal);
......@@ -9,8 +9,6 @@ import moment from "moment";
import * as Urls from "metabase/lib/urls";
import { getSortedUsers } from "../selectors";
import AdminPaneLayout from "metabase/components/AdminPaneLayout.jsx";
import EntityMenu from "metabase/components/EntityMenu";
import Icon from "metabase/components/Icon.jsx";
......@@ -19,15 +17,18 @@ import Radio from "metabase/components/Radio";
import Tooltip from "metabase/components/Tooltip.jsx";
import UserAvatar from "metabase/components/UserAvatar.jsx";
import { entityListLoader } from "metabase/entities/containers/EntityListLoader";
import UserGroupSelect from "../components/UserGroupSelect.jsx";
import { loadMemberships, createMembership, deleteMembership } from "../people";
import { getSortedUsersWithMemberships } from "../selectors";
import { getUser } from "metabase/selectors/user";
import User from "metabase/entities/users";
import Group from "metabase/entities/groups";
const mapStateToProps = (state, props) => ({
users: getSortedUsers(state, props),
user: state.currentUser,
users: getSortedUsersWithMemberships(state, props),
user: getUser(state),
});
const mapDispatchToProps = {
......@@ -37,14 +38,13 @@ const mapDispatchToProps = {
};
// set outer loadingAndErrorWrapper to false to avoid conflicets. the second loader will handle that
@entityListLoader({
entityType: "users",
entityQuery: () => ({ include_deactivated: true }),
@User.listLoader({
query: { include_deactivated: true },
loadingAndErrorWrapper: false,
wrapped: true,
reload: true,
})
@entityListLoader({ entityType: "groups" })
@Group.listLoader()
@connect(mapStateToProps, mapDispatchToProps)
export default class PeopleListingApp extends Component {
state = {};
......@@ -68,9 +68,12 @@ export default class PeopleListingApp extends Component {
}
render() {
let { users, groups } = this.props;
let { user, users, groups } = this.props;
let { showDeactivated } = this.state;
const isAdmin = u => u.is_superuser;
const isCurrentUser = u => user && user.id === u.id;
// TODO - this should be done in connect
users = _.values(users).sort((a, b) => b.date_joined - a.date_joined);
......@@ -197,10 +200,11 @@ export default class PeopleListingApp extends Component {
title: t`Reset password`,
link: Urls.resetPassword(user.id),
},
!user.is_admin && {
title: t`Deactivate user`,
link: Urls.deactivateUser(user.id),
},
!isAdmin(user) &&
!isCurrentUser(user) && {
title: t`Deactivate user`,
link: Urls.deactivateUser(user.id),
},
]}
/>
</td>,
......
import React from "react";
import { connect } from "react-redux";
import { Box } from "grid-styled";
import { t } from "c-3po";
import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader";
import _ from "underscore";
import { reactivateUser, deactivateUser } from "metabase/admin/people/people";
import User from "metabase/entities/users";
import Button from "metabase/components/Button";
import ModalContent from "metabase/components/ModalContent";
import Text from "metabase/components/Text";
@entityObjectLoader({
entityType: "users",
entityId: (state, props) => props.params.userId,
entityQuery: () => ({ include_deactivated: true }),
// NOTE: we have to load the list of users because /api/user/:id doesn't return deactivated users
// but that's ok because it's probably already loaded through the people PeopleListingApp
@User.listLoader({
query: { include_deactivated: true },
wrapped: true,
})
@connect(null, { reactivateUser, deactivateUser })
@connect((state, { users, params: { userId } }) => ({
user: _.findWhere(users, { id: parseInt(userId) }),
}))
class UserActivationModal extends React.Component {
render() {
const { object, onClose, deactivateUser, reactivateUser } = this.props;
const { user, onClose } = this.props;
if (!user) {
return null;
}
const user = object;
const action = user.is_active ? deactivateUser : reactivateUser;
const actionText = user.is_active ? t`Deactivate` : t`Reactivate`;
const title = user && `${actionText} ${user.first_name}?`;
return (
<ModalContent title={title} onClose={onClose}>
<Text>{t`They won't be able to log in anymore`}</Text>
<Button ml="auto" danger onClick={() => action(user) && onClose()}>
{actionText}
</Button>
</ModalContent>
);
if (user.is_active) {
return (
<ModalContent
title={t`Deactivate ${user.getName()}?`}
onClose={onClose}
>
<Text>{t`${user.getName()} won't be able to log in anymore.`}</Text>
<Button
ml="auto"
danger
onClick={() => user.deactivate() && onClose()}
>
{t`Deactivate`}
</Button>
</ModalContent>
);
} else {
return (
<ModalContent
title={t`Reactivate ${user.getName()}?`}
onClose={onClose}
>
<Text>
{t`They'll be able to log in again, and they'll be placed back into the groups they were in before their account was deactivated.`}
</Text>
<Button
ml="auto"
danger
onClick={() => user.reactivate() && onClose()}
>
{t`Reactivate`}
</Button>
</ModalContent>
);
}
}
}
......
import React from "react";
import { t } from "c-3po";
import EntityForm from "metabase/entities/containers/EntityForm";
import ModalContent from "metabase/components/ModalContent";
const UserForm = ({ user, onClose, ...props }) => (
<ModalContent
title={user && user.id != null ? t`Edit user` : t`New user`}
onClose={onClose}
>
<EntityForm entityType="users" entityObject={user} {...props} />
</ModalContent>
);
export default UserForm;
import React from "react";
import { compose } from "redux";
import { connect } from "react-redux";
import { goBack } from "react-router-redux";
import { t } from "c-3po";
import { Flex } from "grid-styled";
import Users from "metabase/entities/users";
import User from "metabase/entities/users";
import { getUserTemporaryPassword } from "../selectors";
import MetabaseSettings from "metabase/lib/settings";
import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader";
import Button from "metabase/components/Button";
import ModalContent from "metabase/components/ModalContent";
import PasswordReveal from "metabase/components/PasswordReveal";
const UserPasswordResetModal = ({
user,
emailConfigured,
goBack,
object,
onPasswordReset,
}) => (
<ModalContent
title={t`Reset ${object.getName()}'s password?`}
onClose={goBack}
>
<p>{t`Are you sure you want to do this?`}</p>
temporaryPassword,
onClose,
}) =>
temporaryPassword ? (
<ModalContent
title={t`${user.first_name}'s password has been reset`}
footer={<Button primary onClick={onClose}>{t`Done`}</Button>}
onClose={onClose}
>
<span className="pb3 block">{t`Here’s a temporary password they can use to log in and then change their password.`}</span>
<Flex>
<Button
ml="auto"
onClick={() => onPasswordReset(object) && goBack()}
danger
>{t`Reset password`}</Button>
</Flex>
</ModalContent>
);
<PasswordReveal password={temporaryPassword} />
</ModalContent>
) : (
<ModalContent
title={t`Reset ${user.getName()}'s password?`}
onClose={onClose}
>
<p>{t`Are you sure you want to do this?`}</p>
export default connect(
(state, props) => ({
emailConfigured: MetabaseSettings.isEmailConfigured(),
}),
{
goBack,
onPasswordReset: Users.actions.passwordResetEmail,
},
)(
entityObjectLoader({
entityType: "users",
entityId: (state, props) => props.params.userId,
<Flex>
<Button
ml="auto"
onClick={async () => {
if (emailConfigured) {
await user.passwordResetEmail();
onClose();
} else {
await user.passwordResetManual();
}
}}
danger
>
{t`Reset password`}
</Button>
</Flex>
</ModalContent>
);
export default compose(
connect(
(state, props) => ({
emailConfigured: MetabaseSettings.isEmailConfigured(),
temporaryPassword: getUserTemporaryPassword(state, {
userId: props.params.userId,
}),
}),
{
onClose: goBack,
},
),
User.loader({
id: (state, props) => props.params.userId,
wrapped: true,
})(UserPasswordResetModal),
);
}),
)(UserPasswordResetModal);
......@@ -5,6 +5,7 @@ import { t, jt } from "c-3po";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { getUserTemporaryPassword } from "../selectors";
import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader";
import Button from "metabase/components/Button";
......@@ -12,18 +13,21 @@ import Link from "metabase/components/Link";
import ModalContent from "metabase/components/ModalContent";
import PasswordReveal from "metabase/components/PasswordReveal";
const EmailSuccess = () => <Box />;
const EmailSuccess = ({ user }) => (
<Box>{jt`We’ve sent an invite to ${(
<strong>{user.email}</strong>
)} with instructions to set their password.`}</Box>
);
const PasswordSuccess = ({ user, password }) => (
const PasswordSuccess = ({ user, temporaryPassword }) => (
<Box>
<Box pb={4}>
{jt`We couldn’t send them an email invitation, so make sure to tell them to log in using ${(
<span className="text-bold">{user.email}</span>
)}
and this password we’ve generated for them:`}
<strong>{user.email}</strong>
)} and this password we’ve generated for them:`}
</Box>
<PasswordReveal password={password} />
<PasswordReveal password={temporaryPassword} />
<Box
style={{ paddingLeft: "5em", paddingRight: "5em" }}
className="pt4 text-centered"
......@@ -37,17 +41,26 @@ const PasswordSuccess = ({ user, password }) => (
</Box>
);
const UserSuccessModal = ({ onClose, object, location }) => (
<ModalContent title={t`Added ${object.getName()}`} onClose={onClose}>
{location.query && location.query.p ? (
<PasswordSuccess user={object} password={location.query.p} />
const UserSuccessModal = ({ onClose, user, temporaryPassword, location }) => (
<ModalContent
title={t`${user.getName()} has been added`}
footer={<Button primary onClick={() => onClose()}>{t`Done`}</Button>}
onClose={onClose}
>
{temporaryPassword ? (
<PasswordSuccess user={user} temporaryPassword={temporaryPassword} />
) : (
<EmailSuccess user={object} />
<EmailSuccess user={user} />
)}
<Button primary onClick={() => onClose()}>{t`Done`}</Button>
</ModalContent>
);
const mapStateToProps = (state, props) => ({
temporaryPassword: getUserTemporaryPassword(state, {
userId: props.params.userId,
}),
});
const mapDispatchToProps = {
onClose: () => push("/admin/people"),
};
......@@ -56,4 +69,4 @@ export default entityObjectLoader({
entityType: "users",
entityId: (state, props) => props.params.userId,
wrapped: true,
})(connect(null, mapDispatchToProps)(UserSuccessModal));
})(connect(mapStateToProps, mapDispatchToProps)(UserSuccessModal));
import {
createAction,
createThunkAction,
handleActions,
combineReducers,
momentifyTimestamps,
} from "metabase/lib/redux";
import MetabaseAnalytics from "metabase/lib/analytics";
import { isMetaBotGroup } from "metabase/lib/groups";
import { SessionApi, UserApi, PermissionsApi } from "metabase/services";
import { PermissionsApi } from "metabase/services";
import _ from "underscore";
import { assoc, dissoc } from "icepick";
export const DEACTIVATE_USER = "metabase/admin/people/DEACTIVATE_USER";
export const REACTIVATE_USER = "metabase/admin/people/REACTIVATE_USER";
export const RESEND_INVITE = "metabase/admin/people/RESEND_INVITE";
export const RESET_PASSWORD_EMAIL =
"metabase/admin/people/RESET_PASSWORD_EMAIL";
export const RESET_PASSWORD_MANUAL =
"metabase/admin/people/RESET_PASSWORD_MANUAL";
export const SHOW_MODAL = "metabase/admin/people/SHOW_MODAL";
export const UPDATE_USER = "metabase/admin/people/UPDATE_USER";
import Users from "metabase/entities/users";
export const LOAD_GROUPS = "metabase/admin/people/LOAD_GROUPS";
export const LOAD_MEMBERSHIPS = "metabase/admin/people/LOAD_MEMBERSHIPS";
export const LOAD_GROUP_DETAILS = "metabase/admin/people/LOAD_GROUP_DETAILS";
......@@ -32,8 +23,6 @@ export const DELETE_MEMBERSHIP = "metabase/admin/people/DELETE_MEMBERSHIP";
// action creators
export const showModal = createAction(SHOW_MODAL);
export const loadGroups = createAction(LOAD_GROUPS, () =>
PermissionsApi.groups(),
);
......@@ -77,85 +66,6 @@ export const deleteMembership = createAction(
},
);
export const deactivateUser = createThunkAction(
DEACTIVATE_USER,
user => async () => {
await UserApi.delete({
userId: user.id,
});
MetabaseAnalytics.trackEvent("People Admin", "User Removed");
// NOTE: DELETE doesn't return the object, so just fake it:
return { ...user, is_active: false };
},
);
export const reactivateUser = createThunkAction(
REACTIVATE_USER,
user => async () => {
const newUser = await UserApi.reactivate({
userId: user.id,
});
MetabaseAnalytics.trackEvent("People Admin", "User Reactivated");
return newUser;
},
);
export const resendInvite = createThunkAction(
RESEND_INVITE,
user => async () => {
MetabaseAnalytics.trackEvent("People Admin", "Resent Invite");
return await UserApi.send_invite({ id: user.id });
},
);
export const resetPasswordManually = createThunkAction(
RESET_PASSWORD_MANUAL,
(user, password) => async () => {
MetabaseAnalytics.trackEvent("People Admin", "Manual Password Reset");
return await UserApi.update_password({ id: user.id, password: password });
},
);
export const resetPasswordViaEmail = createThunkAction(
RESET_PASSWORD_EMAIL,
user => async () => {
MetabaseAnalytics.trackEvent("People Admin", "Trigger User Password Reset");
return await SessionApi.forgot_password({ email: user.email });
},
);
const modal = handleActions(
{
[SHOW_MODAL]: { next: (state, { payload }) => payload },
},
null,
);
const TIMESTAMP_KEYS = [
"date_joined",
"last_login",
"updated_at",
"created_at",
];
const users = handleActions(
{
[DEACTIVATE_USER]: {
next: (state, { payload: user }) =>
assoc(state, user.id, momentifyTimestamps(user, TIMESTAMP_KEYS)),
},
[REACTIVATE_USER]: {
next: (state, { payload: user }) =>
assoc(state, user.id, momentifyTimestamps(user, TIMESTAMP_KEYS)),
},
},
null,
);
const groups = handleActions(
{
[LOAD_GROUPS]: {
......@@ -189,9 +99,26 @@ const group = handleActions(
null,
);
const temporaryPasswords = handleActions(
{
[Users.actionTypes.CREATE]: {
next: (state, { payload }) => ({
...state,
[payload.id]: payload.password,
}),
},
[Users.actionTypes.PASSWORD_RESET_MANUAL]: {
next: (state, { payload }) => ({
...state,
[payload.id]: payload.password,
}),
},
},
{},
);
export default combineReducers({
modal,
users,
temporaryPasswords,
groups,
group,
memberships,
......
......@@ -3,10 +3,9 @@ import _ from "underscore";
export const getGroups = state => state.admin.people.groups;
export const getGroup = state => state.admin.people.group;
export const getModal = state => state.admin.people.modal;
export const getMemberships = state => state.admin.people.memberships;
export const getUsers = createSelector(
export const getUsersWithMemberships = createSelector(
[state => state.entities.users, getMemberships],
(users, memberships) =>
users &&
......@@ -28,8 +27,8 @@ export const getUsers = createSelector(
const compareNames = (a, b) =>
a.localeCompare(b, undefined, { sensitivty: "base" });
export const getSortedUsers = createSelector(
[getUsers],
export const getSortedUsersWithMemberships = createSelector(
[getUsersWithMemberships],
users =>
users &&
_.values(users).sort(
......@@ -38,3 +37,6 @@ export const getSortedUsers = createSelector(
compareNames(a.first_name, b.first_name),
),
);
export const getUserTemporaryPassword = (state, props) =>
state.admin.people.temporaryPasswords[props.userId];
/* @flow */
import { createEntity } from "metabase/lib/entities";
import { t } from "c-3po";
import { assocIn } from "icepick";
import MetabaseAnalytics from "metabase/lib/analytics";
import MetabaseSettings from "metabase/lib/settings";
import MetabaseUtils from "metabase/lib/utils";
import { SessionApi } from "metabase/services";
import { createEntity } from "metabase/lib/entities";
import { UserApi, SessionApi } from "metabase/services";
const User = createEntity({
const DEACTIVATE = "metabase/entities/users/DEACTIVATE";
const REACTIVATE = "metabase/entities/users/REACTIVATE";
const PASSWORD_RESET_EMAIL = "metabase/entities/users/PASSWORD_RESET_EMAIL";
const PASSWORD_RESET_MANUAL = "metabase/entities/users/RESET_PASSWORD_MANUAL";
const RESEND_INVITE = "metabase/entities/users/RESEND_INVITE";
const Users = createEntity({
name: "users",
nameOne: "user",
path: "/api/user",
objectSelectors: {
getName: user => `${user.first_name} ${user.last_name}`,
},
actionTypes: {
DEACTIVATE,
REACTIVATE,
PASSWORD_RESET_EMAIL,
PASSWORD_RESET_MANUAL,
RESEND_INVITE
},
actionDecorators: {
create: {
// if the instance doesn't have
......@@ -30,10 +50,49 @@ const User = createEntity({
},
},
},
objectActions: {
resentInvite: async ({ id }) => {
MetabaseAnalytics.trackEvent("People Admin", "Resent Invite");
await UserApi.send_invite({ id });
return { type: RESEND_INVITE };
},
passwordResetEmail: async ({ email }) => {
return await SessionApi.forgot_password({ email });
MetabaseAnalytics.trackEvent(
"People Admin",
"Trigger User Password Reset",
);
await SessionApi.forgot_password({ email });
return { type: PASSWORD_RESET_EMAIL };
},
passwordResetManual: async ({ id }, password = MetabaseUtils.generatePassword()) => {
MetabaseAnalytics.trackEvent("People Admin", "Manual Password Reset");
await UserApi.update_password({ id, password });
return { type: PASSWORD_RESET_MANUAL, payload: { id, password } };
},
deactivate: async ({ id }) => {
MetabaseAnalytics.trackEvent("People Admin", "User Removed");
// TODO: move these APIs from services to this file
await UserApi.delete({ userId: id });
return { type: DEACTIVATE, payload: { id } };
},
reactivate: async ({ id }) => {
MetabaseAnalytics.trackEvent("People Admin", "User Reactivated");
// TODO: move these APIs from services to this file
const user = await UserApi.reactivate({ userId: id });
return { type: REACTIVATE, payload: user };
},
},
reducer: (state = {}, { type, payload, error }) => {
if (type === DEACTIVATE && !error) {
return assocIn(state, [payload.id, "is_active"], false);
} else if (type === REACTIVATE && !error) {
return assocIn(state, [payload.id, "is_active"], true);
} else if (type === PASSWORD_RESET_MANUAL && !error) {
return assocIn(state, [payload.id, "password"], payload.password);
}
return state;
},
form: {
......@@ -61,4 +120,4 @@ const User = createEntity({
},
});
export default User;
export default Users;
......@@ -145,8 +145,8 @@ export function resetPassword(userId) {
return `/admin/people/${userId}/reset`;
}
export function newUserSuccess(userId, pass) {
return `/admin/people/${userId}/success?p=${pass}`;
export function newUserSuccess(userId) {
return `/admin/people/${userId}/success`;
}
export function deactivateUser(userId) {
......
import React from "react";
import mock from "xhr-mock";
import {
mountWithStore,
fillAndSubmitForm,
getFormValues,
} from "__support__/integration_tests";
import { click, clickButton } from "__support__/enzyme_utils";
import { delay } from "metabase/lib/promise";
import UserActivationModal from "metabase/admin/people/containers/UserActivationModal";
const MOCK_USER = {
first_name: "Testy",
last_name: "McTestFace",
email: "test@metabase.com",
is_active: true,
};
describe("UserActivationModal", () => {
beforeEach(() => mock.setup());
afterEach(() => mock.teardown());
describe("with active user", () => {
it("should deactivate the user", async () => {
expect.assertions(1);
// NOTE: currently loads the list of users since deactivated users return 404 from /api/user/:id
mock.get("/api/user?include_deactivated=true", (req, res) =>
res.json([{ id: 42, ...MOCK_USER }]),
);
mock.delete("/api/user/42", (req, res) => {
return res.json({ success: true });
});
const onClose = jest.fn();
const { wrapper, store } = mountWithStore(
<UserActivationModal params={{ userId: 42 }} onClose={onClose} />,
);
const deactivateButton = await wrapper.async.find(".Button--danger");
clickButton(deactivateButton);
expect(onClose).toHaveBeenCalled();
});
});
describe("with deactivated user", () => {
it("reactivate the user", async () => {
expect.assertions(1);
// NOTE: currently loads the list of users since deactivated users return 404 from /api/user/:id
mock.get("/api/user?include_deactivated=true", (req, res) =>
res.json([{ id: 42, ...MOCK_USER, is_active: false }]),
);
mock.put("/api/user/42/reactivate", (req, res) => {
return res.json({ id: 42, ...MOCK_USER });
});
const onClose = jest.fn();
const { wrapper, store } = mountWithStore(
<UserActivationModal params={{ userId: 42 }} onClose={onClose} />,
);
const resetButton = await wrapper.async.find(".Button--danger");
clickButton(resetButton);
expect(onClose).toHaveBeenCalled();
});
});
});
import React from "react";
import mock from "xhr-mock";
import {
mountWithStore,
fillAndSubmitForm,
getFormValues,
} from "__support__/integration_tests";
import { click, clickButton } from "__support__/enzyme_utils";
import { delay } from "metabase/lib/promise";
import UserPasswordResetModal from "metabase/admin/people/containers/UserPasswordResetModal";
import MetabaseSettings from "metabase/lib/settings";
const MOCK_USER = {
first_name: "Testy",
last_name: "McTestFace",
email: "test@metabase.com",
};
describe("UserPasswordResetModal", () => {
beforeEach(() => mock.setup());
afterEach(() => mock.teardown());
describe("with email not configured", () => {
beforeEach(() => {
MetabaseSettings.isEmailConfigured = () => false;
});
it("should change the user's password to random password", async () => {
expect.assertions(3);
let newPassword;
mock.get("/api/user/42", (req, res) =>
res.json({ id: 42, ...MOCK_USER }),
);
mock.put("/api/user/42/password", (req, res) => {
expect(Object.keys(req.json())).toEqual(["password"]);
newPassword = req.json().password;
return res.json({ id: 42, ...MOCK_USER });
});
const { wrapper, store } = mountWithStore(
<UserPasswordResetModal params={{ userId: 42 }} />,
);
const resetButton = await wrapper.async.find(".Button--danger");
expect(resetButton.length).toBe(1);
clickButton(resetButton);
const showPasswordLink = await wrapper.async.find(".link");
click(showPasswordLink);
const passwordReveal = await wrapper.async.find("input");
expect(passwordReveal.props().value).toBe(newPassword);
});
});
describe("with email configured", () => {
beforeEach(() => {
MetabaseSettings.isEmailConfigured = () => true;
});
it("should trigger a password reset email", async () => {
expect.assertions(1);
mock.get("/api/user/42", (req, res) =>
res.json({ id: 42, ...MOCK_USER }),
);
mock.post("/api/session/forgot_password", (req, res) => {
expect(req.json().email).toEqual(MOCK_USER.email);
return res.json({});
});
const { wrapper, store } = mountWithStore(
<UserPasswordResetModal params={{ userId: 42 }} />,
);
const resetButton = await wrapper.async.find(".Button--danger");
clickButton(resetButton);
await store.waitForAction("@@router/CALL_HISTORY_METHOD");
});
});
});
......@@ -15,7 +15,7 @@ import {
import ModalContent from "metabase/components/ModalContent";
import { delay } from "metabase/lib/promise";
import Button from "metabase/components/Button";
import { getUsers } from "metabase/admin/people/selectors";
import { getUsersWithMemberships } from "metabase/admin/people/selectors";
import UserGroupSelect from "metabase/admin/people/components/UserGroupSelect";
import { GroupOption } from "metabase/admin/people/components/GroupSelect";
import { UserApi } from "metabase/services";
......@@ -69,7 +69,7 @@ describe("admin/people", () => {
await delay(100);
// it should be a pretty safe assumption in test environment that the user that was just created has the biggest ID
const userIds = Object.keys(getUsers(store.getState()));
const userIds = Object.keys(getUsersWithMemberships(store.getState()));
createdUserId = Math.max.apply(null, userIds.map(key => parseInt(key)));
const userCreatedModal = app.find(ModalContent);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment