[CORL-155] User Suspending and Banning (#2247)

* feat: suspending, banning, now propogation

* feat: adapting to `now`

* feat: support auth for suspension/banned

* feat: added trace-id to requests

* feat: new mutation api with hooks support

* feat: added user status filtering, current field

* feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3

* fix: typo

* fix: tests

* chore: rename banned status to ban status

* test: feature test + lots of test helper improvements e.g. types

* fix: add translation to ban user modal

* fix: translation

* fix: test
This commit is contained in:
Wyatt Johnson
2019-04-22 22:57:32 +00:00
committed by GitHub
parent b63c00f26f
commit dbbc1af42e
147 changed files with 4609 additions and 1468 deletions
@@ -29,7 +29,7 @@ async function createTestRenderer(
.stub()
.returns(
merge(
{ ...users[0], email: "", username: "", profiles: [] },
{ ...users.admins[0], email: "", username: "", profiles: [] },
get(customResolver, "Query.viewer")
)
),
@@ -25,7 +25,7 @@ async function createTestRenderer(
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
viewer: sinon.stub().returns({ ...users[0], email: "" }),
viewer: sinon.stub().returns({ ...users.admins[0], email: "" }),
},
};
@@ -25,7 +25,7 @@ async function createTestRenderer(
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
viewer: sinon.stub().returns({ ...users[0], profiles: [] }),
viewer: sinon.stub().returns({ ...users.admins[0], profiles: [] }),
},
};
@@ -18,7 +18,7 @@ const resolvers = {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
@@ -32,7 +32,7 @@ function createTestRenderer(
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns({ ...users[0], ...userDiff }),
viewer: sinon.stub().returns({ ...users.admins[0], ...userDiff }),
},
};
const { testRenderer, context } = create({
@@ -21,7 +21,7 @@ const resolvers = {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
@@ -47,7 +47,7 @@ it("logs out", async () => {
.returns({});
const userMenu = await waitForElement(() =>
within(testRenderer.root).getByText(users[0].username, {
within(testRenderer.root).getByText(users.admins[0].username!, {
selector: "button",
})
);
@@ -1,5 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ban user 1`] = `
<div
aria-labelledby="banModal-title"
className="Modal-root"
onKeyDown={[Function]}
role="modal"
>
<div
className="Backdrop-root Backdrop-active"
data-testid="backdrop"
onClick={[Function]}
/>
<div
onFocus={[Function]}
tabIndex={0}
/>
<div
tabIndex={-1}
/>
<div
className="Card-root BanModal-card"
>
<button
className="BaseButton-root CloseButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md CloseButton-icon"
>
close
</span>
</button>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-header2 Typography-colorTextPrimary"
id="banModal-title"
>
Are you sure you want to ban
<strong>
Isabelle
</strong>
?
</h1>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Once banned, this user will no longer be able to comment, use
reactions, or report comments.
</p>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Cancel
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Ban User
</button>
</div>
</div>
</div>
<div
onFocus={[Function]}
tabIndex={0}
/>
</div>
`;
exports[`renders community 1`] = `
<div
className="MainLayout-root Community-root"
@@ -68,63 +167,111 @@ exports[`renders community 1`] = `
>
Show Me
</legend>
<span
className="SelectField-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
<select
aria-label="Search by role"
className="SelectField-select"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value=""
<span
className="SelectField-root UserTableFilter-selectField"
>
<option
<select
aria-label="Search by role"
className="SelectField-select"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value=""
>
Everyone
</option>
<optgroup
label="Audience"
>
<option
value="COMMENTER"
value=""
>
Commenters
All Roles
</option>
</optgroup>
<optgroup
label="Organization"
>
<option
value="ADMIN"
<optgroup
label="Audience"
>
Admins
</option>
<option
value="MODERATOR"
<option
value="COMMENTER"
>
Commenters
</option>
</optgroup>
<optgroup
label="Organization"
>
Moderators
</option>
<option
value="STAFF"
>
Staff
</option>
</optgroup>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper"
>
<option
value="ADMIN"
>
Admins
</option>
<option
value="MODERATOR"
>
Moderators
</option>
<option
value="STAFF"
>
Staff
</option>
</optgroup>
</select>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
aria-hidden={true}
className="SelectField-afterWrapper"
>
expand_more
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
expand_more
</span>
</span>
</span>
</span>
<span
className="SelectField-root UserTableFilter-selectField"
>
<select
aria-label="Search by user status"
className="SelectField-select"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value=""
>
<option
value=""
>
All Statuses
</option>
<option
value="ACTIVE"
>
Active
</option>
<option
value="SUSPENDED"
>
Suspended
</option>
<option
value="BANNED"
>
Banned
</option>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
expand_more
</span>
</span>
</span>
</div>
</fieldset>
</div>
<div
@@ -159,6 +306,11 @@ exports[`renders community 1`] = `
>
Role
</th>
<th
className="TableCell-root UserTable-statusColumn TableCell-header"
>
Status
</th>
</tr>
</thead>
<tbody
@@ -190,11 +342,40 @@ exports[`renders community 1`] = `
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<span
className="RoleText-root"
<div
className="ButtonPadding-root"
>
Admin
</span>
<span
className="RoleText-root"
>
Admin
</span>
</div>
</td>
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<div
className="ButtonPadding-root"
>
<div
className="Typography-root Typography-detail Typography-colorSuccess"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
>
<div
aria-hidden={true}
className="UserStatus-dot"
>
</div>
<div>
Active
</div>
</div>
</div>
</div>
</td>
</tr>
<tr
@@ -264,6 +445,248 @@ exports[`renders community 1`] = `
</div>
</div>
</td>
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<div
className="ButtonPadding-root"
>
<div
className="Typography-root Typography-detail Typography-colorSuccess"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
>
<div
aria-hidden={true}
className="UserStatus-dot"
>
</div>
<div>
Active
</div>
</div>
</div>
</div>
</td>
</tr>
<tr
className="TableRow-root TableRow-body"
>
<td
className="TableCell-root UserRow-usernameColumn TableCell-body"
>
Huy
</td>
<td
className="TableCell-root UserRow-emailColumn TableCell-body"
>
<a
className="TextLink-root"
href="mailto:huy@test.com"
>
huy@test.com
</a>
</td>
<td
className="TableCell-root UserRow-memberSinceColumn TableCell-body"
>
07/06/2018
</td>
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change role"
className="BaseButton-root Button-root RoleChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="RoleText-root"
>
Staff
</span>
<span
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="community-roleChange-ariainfo"
id="community-roleChange"
role="popup"
>
<div
className="AriaInfo-root"
id="community-roleChange-ariainfo"
>
A dropdown to change the user role
</div>
</div>
</div>
</td>
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<div
className="ButtonPadding-root"
>
<div
className="Typography-root Typography-detail Typography-colorSuccess"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
>
<div
aria-hidden={true}
className="UserStatus-dot"
>
</div>
<div>
Active
</div>
</div>
</div>
</div>
</td>
</tr>
<tr
className="TableRow-root TableRow-body"
>
<td
className="TableCell-root UserRow-usernameColumn TableCell-body"
>
Isabelle
</td>
<td
className="TableCell-root UserRow-emailColumn TableCell-body"
>
<a
className="TextLink-root"
href="mailto:isabelle@test.com"
>
isabelle@test.com
</a>
</td>
<td
className="TableCell-root UserRow-memberSinceColumn TableCell-body"
>
07/06/2018
</td>
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change role"
className="BaseButton-root Button-root RoleChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="RoleText-root RoleText-commenter"
>
Commenter
</span>
<span
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="community-roleChange-ariainfo"
id="community-roleChange"
role="popup"
>
<div
className="AriaInfo-root"
id="community-roleChange-ariainfo"
>
A dropdown to change the user role
</div>
</div>
</div>
</td>
<td
className="TableCell-root UserRow-roleColumn TableCell-body"
>
<div
className="Popover-root"
>
<button
aria-label="Change user status"
className="BaseButton-root Button-root UserStatusChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<div
className="Typography-root Typography-detail Typography-colorSuccess"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
>
<div
aria-hidden={true}
className="UserStatus-dot"
>
</div>
<div>
Active
</div>
</div>
</div>
<span
aria-hidden="true"
className="Icon-root Icon-lg ButtonIcon-root"
>
arrow_drop_down
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="community-statusChange-ariainfo"
id="community-statusChange"
role="popup"
>
<div
className="AriaInfo-root"
id="community-statusChange-ariainfo"
>
A dropdown to change the user status
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
@@ -340,63 +763,111 @@ exports[`renders empty community 1`] = `
>
Show Me
</legend>
<span
className="SelectField-root"
<div
className="Flex-root Flex-flex Flex-itemGutter"
>
<select
aria-label="Search by role"
className="SelectField-select"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value=""
<span
className="SelectField-root UserTableFilter-selectField"
>
<option
<select
aria-label="Search by role"
className="SelectField-select"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value=""
>
Everyone
</option>
<optgroup
label="Audience"
>
<option
value="COMMENTER"
value=""
>
Commenters
All Roles
</option>
</optgroup>
<optgroup
label="Organization"
>
<option
value="ADMIN"
<optgroup
label="Audience"
>
Admins
</option>
<option
value="MODERATOR"
<option
value="COMMENTER"
>
Commenters
</option>
</optgroup>
<optgroup
label="Organization"
>
Moderators
</option>
<option
value="STAFF"
>
Staff
</option>
</optgroup>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper"
>
<option
value="ADMIN"
>
Admins
</option>
<option
value="MODERATOR"
>
Moderators
</option>
<option
value="STAFF"
>
Staff
</option>
</optgroup>
</select>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
aria-hidden={true}
className="SelectField-afterWrapper"
>
expand_more
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
expand_more
</span>
</span>
</span>
</span>
<span
className="SelectField-root UserTableFilter-selectField"
>
<select
aria-label="Search by user status"
className="SelectField-select"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value=""
>
<option
value=""
>
All Statuses
</option>
<option
value="ACTIVE"
>
Active
</option>
<option
value="SUSPENDED"
>
Suspended
</option>
<option
value="BANNED"
>
Banned
</option>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
expand_more
</span>
</span>
</span>
</div>
</fieldset>
</div>
<div
@@ -431,6 +902,11 @@ exports[`renders empty community 1`] = `
>
Role
</th>
<th
className="TableCell-root UserTable-statusColumn TableCell-header"
>
Status
</th>
</tr>
</thead>
<tbody
@@ -1,10 +1,16 @@
import { get, merge } from "lodash";
import { merge } from "lodash";
import TestRenderer from "react-test-renderer";
import sinon from "sinon";
import { GQLUSER_ROLE } from "talk-framework/schema";
import {
createSinonStub,
GQLUSER_ROLE,
GQLUSER_STATUS,
QueryToSettingsResolver,
QueryToUsersResolver,
QueryToViewerResolver,
} from "talk-framework/schema";
import {
createMutationResolverStub,
createQueryResolverStub,
findParentWithType,
replaceHistoryLocation,
waitForElement,
@@ -12,6 +18,11 @@ import {
within,
} from "talk-framework/testHelpers";
import {
BanUserMutation,
RemoveUserBanMutation,
UpdateUserRoleMutation,
} from "talk-admin/mutations";
import create from "../create";
import {
communityUsers,
@@ -28,14 +39,16 @@ const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
users: sinon.stub().callsFake((_, data) => {
expectAndFail(data.role).toBeFalsy();
settings: createQueryResolverStub<QueryToSettingsResolver>(
() => settings
),
users: createQueryResolverStub<QueryToUsersResolver>(variables => {
expectAndFail(variables.role).toBeFalsy();
return communityUsers;
}),
viewer: sinon.stub().returns(users[0]),
viewer: createQueryResolverStub<QueryToViewerResolver>(
() => users.admins[0]
),
...resolver.Query,
},
};
@@ -61,7 +74,9 @@ it("renders community", async () => {
it("renders empty community", async () => {
const { container } = await createTestRenderer({
Query: {
users: sinon.stub().returns(emptyCommunityUsers),
users: createQueryResolverStub<QueryToUsersResolver>(
() => emptyCommunityUsers
),
},
});
expect(within(container).toJSON()).toMatchSnapshot();
@@ -70,13 +85,16 @@ it("renders empty community", async () => {
it("filter by role", async () => {
const { container } = await createTestRenderer({
Query: {
users: createSinonStub(
s => s.onFirstCall().returns(communityUsers),
s =>
s.onSecondCall().callsFake((_, data) => {
expectAndFail(data.role).toBe(GQLUSER_ROLE.COMMENTER);
return emptyCommunityUsers;
})
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
switch (callCount) {
case 0:
return communityUsers;
default:
expectAndFail(variables.role).toBe(GQLUSER_ROLE.COMMENTER);
return emptyCommunityUsers;
}
}
),
},
});
@@ -98,26 +116,27 @@ it("filter by role", async () => {
});
it("can't change viewer role", async () => {
const viewer = users[0];
const viewer = users.admins[0];
const { container } = await createTestRenderer();
const viewerRow = within(container).getByText(viewer.username, {
const viewerRow = within(container).getByText(viewer.username!, {
selector: "tr",
});
expect(() => within(viewerRow).getByLabelText("Change role")).toThrow();
});
it("change user role", async () => {
const user = users[1];
const updateUserRole = sinon.stub().callsFake((_: any, data: any) => {
expectAndFail(data.input).toMatchObject({
const user = users.commenters[0];
const updateUserRole = createMutationResolverStub<
typeof UpdateUserRoleMutation
>(variables => {
expectAndFail(variables).toMatchObject({
userID: user.id,
role: GQLUSER_ROLE.STAFF,
});
const userRecord = merge({}, user, { role: data.input.role });
const userRecord = merge({}, user, { role: variables.role });
return {
user: userRecord,
clientMutationId: data.input.clientMutationId,
};
});
@@ -125,7 +144,7 @@ it("change user role", async () => {
Mutation: { updateUserRole },
});
const userRow = within(container).getByText(user.username, {
const userRow = within(container).getByText(user.username!, {
selector: "tr",
});
@@ -150,10 +169,10 @@ it("change user role", async () => {
});
it("can't change role as a moderator", async () => {
const viewer = users[1];
const viewer = users.moderators[0];
const { container } = await createTestRenderer({
Query: {
viewer: sinon.stub().returns(viewer),
viewer: createQueryResolverStub<QueryToViewerResolver>(() => viewer),
},
});
expect(() => within(container).getByLabelText("Change role")).toThrow();
@@ -162,20 +181,38 @@ it("can't change role as a moderator", async () => {
it("load more", async () => {
const { container } = await createTestRenderer({
Query: {
users: createSinonStub(
s =>
s.onFirstCall().returns({
edges: [
{ node: users[0], cursor: users[0].createdAt },
{ node: users[1], cursor: users[1].createdAt },
],
pageInfo: { endCursor: users[1].createdAt, hasNextPage: true },
}),
s =>
s.onSecondCall().returns({
edges: [{ node: users[2], cursor: users[2].createdAt }],
pageInfo: { endCursor: users[2].createdAt, hasNextPage: false },
})
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
switch (callCount) {
case 0:
return {
edges: [
{ node: users.admins[0], cursor: users.admins[0].createdAt },
{
node: users.commenters[0],
cursor: users.commenters[0].createdAt,
},
],
pageInfo: {
endCursor: users.commenters[0].createdAt,
hasNextPage: true,
},
};
default:
return {
edges: [
{
node: users.commenters[1],
cursor: users.commenters[1].createdAt,
},
],
pageInfo: {
endCursor: users.commenters[1].createdAt,
hasNextPage: false,
},
};
}
}
),
},
});
@@ -188,19 +225,22 @@ it("load more", async () => {
await waitUntilThrow(() => within(container).getByText("Load More"));
// Make sure third user was added.
within(container).getByText(users[2].username);
within(container).getByText(users.commenters[1].username!);
});
it("filter by search", async () => {
const { container } = await createTestRenderer({
Query: {
users: createSinonStub(
s => s.onFirstCall().returns(communityUsers),
s =>
s.onSecondCall().callsFake((_, data) => {
expectAndFail(data.query).toBe("search");
return emptyCommunityUsers;
})
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
switch (callCount) {
case 0:
return communityUsers;
default:
expectAndFail(variables.query).toBe("search");
return emptyCommunityUsers;
}
}
),
},
});
@@ -221,3 +261,170 @@ it("filter by search", async () => {
within(container).getByText("could not find anyone", { exact: false })
);
});
it("filter by status", async () => {
const { container } = await createTestRenderer({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
switch (callCount) {
case 0:
return communityUsers;
default:
expectAndFail(variables.status).toBe("BANNED");
return emptyCommunityUsers;
}
}
),
},
});
const statusField = within(container).getByLabelText(
"Search by user status",
{
exact: false,
}
);
const bannedOption = within(statusField).getByText("Banned");
TestRenderer.act(() => {
statusField.props.onChange({
target: { value: bannedOption.props.value.toString() },
});
// TODO: Fix act warnings until await Promise.resolve();
// or whatever comes out at https://github.com/facebook/react/issues/14769
});
await waitForElement(() =>
within(container).getByText("We could not find anyone", { exact: false })
);
});
it("can't change staff, moderator and admin status", async () => {
const { container } = await createTestRenderer();
["Admin", "Moderator", "Staff"].forEach(role => {
const viewerRow = within(container).getByText(role, {
selector: "tr",
});
expect(() =>
within(viewerRow).getByLabelText("Change user status")
).toThrow();
});
});
it("ban user", async () => {
const user = users.commenters[0];
const banUser = createMutationResolverStub<typeof BanUserMutation>(
variables => {
expectAndFail(variables).toMatchObject({
userID: user.id,
});
const userRecord = merge({}, user, {
status: {
current: user.status.current.concat(GQLUSER_STATUS.BANNED),
banned: { active: true },
},
});
return {
user: userRecord,
};
}
);
const { container, testRenderer } = await createTestRenderer({
Mutation: { banUser },
});
const userRow = within(container).getByText(user.username!, {
selector: "tr",
});
TestRenderer.act(() => {
within(userRow)
.getByLabelText("Change user status")
.props.onClick();
});
const popup = within(userRow).getByLabelText(
"A dropdown to change the user status"
);
TestRenderer.act(() => {
within(popup)
.getByText("Ban User")
.props.onClick();
});
const modal = within(testRenderer.root).getByLabelText(
"Are you sure you want to ban",
{
exact: false,
}
);
expect(within(modal).toJSON()).toMatchSnapshot();
within(modal)
.getByText("Ban User")
.props.onClick();
within(userRow).getByText("Banned");
expect(banUser.called).toBe(true);
});
it("remove user ban", async () => {
const user = users.bannedCommenter;
const removeUserBan = createMutationResolverStub<
typeof RemoveUserBanMutation
>(variables => {
expectAndFail(variables).toMatchObject({
userID: user.id,
});
const userRecord = merge({}, user, {
status: {
current: user.status.current.filter(s => s !== GQLUSER_STATUS.BANNED),
banned: { active: false },
},
});
return {
user: userRecord,
};
});
const { container } = await createTestRenderer({
Mutation: { removeUserBan },
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(() => ({
edges: [
{
node: user,
cursor: user.createdAt,
},
],
pageInfo: { endCursor: null, hasNextPage: false },
})),
},
});
const userRow = within(container).getByText(user.username!, {
selector: "tr",
});
TestRenderer.act(() => {
within(userRow)
.getByLabelText("Change user status")
.props.onClick();
});
const popup = within(userRow).getByLabelText(
"A dropdown to change the user status"
);
TestRenderer.act(() => {
within(popup)
.getByText("Remove Ban")
.props.onClick();
});
within(userRow).getByText("Active");
expect(removeUserBan.called).toBe(true);
});
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -28,7 +28,7 @@ const createTestRenderer = async (resolver: any = {}) => {
.returns(
merge({}, settingsWithEmptyAuth, get(resolver, "Query.settings"))
),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -29,7 +29,7 @@ const createTestRenderer = async (
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -47,7 +47,7 @@ it("denies access to moderators", async () => {
for (const r of deniedRoles) {
const { testRenderer } = await createTestRenderer({
Query: {
viewer: sinon.stub().returns({ ...users[0], role: r }),
viewer: sinon.stub().returns({ ...users.admins[0], role: r }),
},
});
await waitForElement(() =>
@@ -61,7 +61,7 @@ it("allows access to admins", async () => {
for (const r of deniedRoles) {
const { testRenderer } = await createTestRenderer({
Query: {
viewer: sinon.stub().returns({ ...users[0], role: r }),
viewer: sinon.stub().returns({ ...users.admins[0], role: r }),
},
});
await waitForElement(() =>
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -62,7 +62,7 @@ const createTestRenderer = async (resolver: any = {}) => {
Query: {
...resolver.Query,
viewer: sinon.stub().returns({
...users[0],
...users.admins[0],
commentModerationActionHistory,
}),
settings: sinon
+240 -159
View File
@@ -1,10 +1,24 @@
import { merge } from "lodash";
import {
GQLComment,
GQLCOMMENT_STATUS,
GQLCommentModerationAction,
GQLCommentsConnection,
GQLMODERATION_MODE,
GQLModerationQueues,
GQLSettings,
GQLStoriesConnection,
GQLStory,
GQLSTORY_STATUS,
GQLUser,
GQLUSER_ROLE,
GQLUSER_STATUS,
GQLUsersConnection,
} from "talk-framework/schema";
import { createFixture, createFixtures } from "talk-framework/testHelpers";
import { GQLStory, GQLSTORY_STATUS, GQLUSER_ROLE } from "talk-framework/schema";
export const settings = {
export const settings = createFixture<GQLSettings>({
id: "settings",
moderation: "POST",
moderation: GQLMODERATION_MODE.POST,
premodLinksEnable: false,
wordList: {
suspect: ["idiot", "stupid"],
@@ -24,7 +38,7 @@ export const settings = {
timeout: 604800,
message: "Comments are closed on this story.",
},
customCSSURL: null,
customCSSURL: "",
domains: ["localhost:8080"],
editCommentWindowLength: 30000,
communityGuidelines: {
@@ -61,7 +75,7 @@ export const settings = {
admin: true,
stream: true,
},
key: null,
key: "",
keyGeneratedAt: null,
},
google: {
@@ -101,71 +115,73 @@ export const settings = {
},
},
},
};
});
export const settingsWithEmptyAuth = {
...settings,
id: "settings",
auth: {
integrations: {
local: {
enabled: true,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
export const settingsWithEmptyAuth = createFixture<GQLSettings>(
{
id: "settings",
auth: {
integrations: {
local: {
enabled: true,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
},
},
sso: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
sso: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
key: "",
keyGeneratedAt: null,
},
key: null,
keyGeneratedAt: null,
},
google: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
google: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "",
clientSecret: "",
callbackURL: "http://localhost/google/callback",
redirectURL: "http://localhost/google",
},
clientID: "",
clientSecret: "",
callbackURL: "http://localhost/google/callback",
redirectURL: "http://localhost/google",
},
facebook: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
facebook: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "",
clientSecret: "",
callbackURL: "http://localhost/facebook/callback",
redirectURL: "http://localhost/facebook",
},
clientID: "",
clientSecret: "",
callbackURL: "http://localhost/facebook/callback",
redirectURL: "http://localhost/facebook",
},
oidc: {
enabled: false,
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
oidc: {
enabled: false,
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
},
name: "",
callbackURL: "http://localhost/oidc/callback",
redirectURL: "http://localhost/oidc",
},
name: "",
callbackURL: "http://localhost/oidc/callback",
redirectURL: "http://localhost/oidc",
},
},
},
};
settings
);
export const moderationActions = [
export const moderationActions = createFixtures<GQLCommentModerationAction>([
{
id: "07e8f815-e165-4b5d-b438-7163415c8cf7",
revision: {
@@ -179,7 +195,7 @@ export const moderationActions = [
},
},
createdAt: "2018-11-29T16:01:51.897Z",
status: "ACCEPTED",
status: GQLCOMMENT_STATUS.ACCEPTED,
__typename: "CommentModerationAction",
},
{
@@ -195,7 +211,7 @@ export const moderationActions = [
},
},
createdAt: "2018-11-29T16:01:45.644Z",
status: "REJECTED",
status: GQLCOMMENT_STATUS.REJECTED,
__typename: "CommentModerationAction",
},
{
@@ -211,7 +227,7 @@ export const moderationActions = [
},
},
createdAt: "2018-11-29T16:01:42.060Z",
status: "ACCEPTED",
status: GQLCOMMENT_STATUS.ACCEPTED,
__typename: "CommentModerationAction",
},
{
@@ -227,7 +243,7 @@ export const moderationActions = [
},
},
createdAt: "2018-11-29T16:01:34.539Z",
status: "REJECTED",
status: GQLCOMMENT_STATUS.REJECTED,
__typename: "CommentModerationAction",
},
{
@@ -243,45 +259,105 @@ export const moderationActions = [
},
},
createdAt: "2018-11-29T16:01:30.648Z",
status: "ACCEPTED",
status: GQLCOMMENT_STATUS.ACCEPTED,
__typename: "CommentModerationAction",
},
];
]);
export const baseUser = {
export const baseUser = createFixture<GQLUser>({
profiles: [{ __typename: "LocalProfile" }],
createdAt: "2018-07-06T18:24:00.000Z",
status: {
current: [],
ban: {
active: false,
history: [],
},
suspension: {
active: false,
history: [],
},
},
});
export const users = {
admins: createFixtures<GQLUser>(
[
{
id: "user-admin-0",
username: "Markus",
email: "markus@test.com",
role: GQLUSER_ROLE.ADMIN,
},
],
baseUser
),
moderators: createFixtures<GQLUser>(
[
{
id: "user-moderator-0",
username: "Lukas",
email: "lukas@test.com",
role: GQLUSER_ROLE.MODERATOR,
},
],
baseUser
),
staff: createFixtures<GQLUser>(
[
{
id: "user-staff-0",
username: "Huy",
email: "huy@test.com",
role: GQLUSER_ROLE.STAFF,
},
],
baseUser
),
commenters: createFixtures<GQLUser>(
[
{
id: "user-commenter-0",
username: "Isabelle",
email: "isabelle@test.com",
role: GQLUSER_ROLE.COMMENTER,
},
{
id: "user-commenter-1",
username: "Ngoc",
email: "ngoc@test.com",
role: GQLUSER_ROLE.COMMENTER,
},
{
id: "user-commenter-2",
username: "Max",
email: "max@test.com",
role: GQLUSER_ROLE.COMMENTER,
},
],
baseUser
),
bannedCommenter: createFixture<GQLUser>(
{
id: "user-banned-0",
username: "Ingrid",
email: "ingrid@test.com",
role: GQLUSER_ROLE.COMMENTER,
status: {
current: [GQLUSER_STATUS.BANNED],
ban: { active: true },
suspension: { active: true },
},
},
baseUser
),
};
export const users = [
{
...baseUser,
id: "user-0",
username: "Markus",
email: "markus@test.com",
role: GQLUSER_ROLE.ADMIN,
},
{
...baseUser,
id: "user-1",
username: "Lukas",
email: "lukas@test.com",
role: GQLUSER_ROLE.MODERATOR,
},
{
...baseUser,
id: "user-2",
username: "Isabelle",
email: "isabelle@test.com",
role: GQLUSER_ROLE.COMMENTER,
},
];
export const baseComment = {
author: users[0],
export const baseComment = createFixture<GQLComment>({
author: users.commenters[0],
body: "Comment Body",
createdAt: "2018-07-06T18:24:00.000Z",
status: "NONE",
status: GQLCOMMENT_STATUS.NONE,
actionCounts: {
flag: {
reasons: {
@@ -296,68 +372,71 @@ export const baseComment = {
},
},
},
};
});
export const reportedComments = [
merge({}, baseComment, {
id: "comment-0",
author: users[0],
revision: {
id: "comment-0-revision-0",
},
permalink: "http://localhost/comment/0",
body:
"This is the last random sentence I will be writing and I am going to stop mid-sent",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 2,
export const reportedComments = createFixtures<GQLComment>(
[
{
id: "comment-0",
author: users.commenters[0],
revision: {
id: "comment-0-revision-0",
},
permalink: "http://localhost/comment/0",
body:
"This is the last random sentence I will be writing and I am going to stop mid-sent",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 2,
},
},
},
},
}),
merge({}, baseComment, {
id: "comment-1",
revision: {
id: "comment-1-revision-1",
},
permalink: "http://localhost/comment/1",
author: users[1],
body: "Don't fool with me",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_OFFENSIVE: 3,
{
id: "comment-1",
revision: {
id: "comment-1-revision-1",
},
permalink: "http://localhost/comment/1",
author: users.commenters[1],
body: "Don't fool with me",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_OFFENSIVE: 3,
},
},
},
},
}),
merge({}, baseComment, {
id: "comment-2",
revision: {
id: "comment-2-revision-2",
},
permalink: "http://localhost/comment/2",
status: "PREMOD",
author: users[2],
body: "I think I deserve better",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 1,
COMMENT_REPORTED_OFFENSIVE: 1,
{
id: "comment-2",
revision: {
id: "comment-2-revision-2",
},
permalink: "http://localhost/comment/2",
status: GQLCOMMENT_STATUS.PREMOD,
author: users.commenters[2],
body: "I think I deserve better",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 1,
COMMENT_REPORTED_OFFENSIVE: 1,
},
},
},
},
}),
];
],
baseComment
);
export const rejectedComments = reportedComments.map(c => ({
export const rejectedComments = reportedComments.map<GQLComment>(c => ({
...c,
status: "REJECTED",
status: GQLCOMMENT_STATUS.REJECTED,
}));
export const emptyModerationQueues = {
export const emptyModerationQueues = createFixture<GQLModerationQueues>({
reported: {
id: "reported",
count: 0,
@@ -373,27 +452,29 @@ export const emptyModerationQueues = {
count: 0,
comments: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
},
};
});
export const emptyRejectedComments = {
export const emptyRejectedComments = createFixture<GQLCommentsConnection>({
edges: [],
pageInfo: { endCursor: null, hasNextPage: false },
};
});
export const communityUsers = {
export const communityUsers = createFixture<GQLUsersConnection>({
edges: [
{ node: users[0], cursor: users[0].createdAt },
{ node: users[1], cursor: users[1].createdAt },
{ node: users.admins[0], cursor: users.admins[0].createdAt },
{ node: users.moderators[0], cursor: users.moderators[0].createdAt },
{ node: users.staff[0], cursor: users.staff[0].createdAt },
{ node: users.commenters[0], cursor: users.commenters[0].createdAt },
],
pageInfo: { endCursor: null, hasNextPage: false },
};
});
export const emptyCommunityUsers = {
export const emptyCommunityUsers = createFixture<GQLUsersConnection>({
edges: [],
pageInfo: { endCursor: null, hasNextPage: false },
};
});
export const stories: Array<Partial<GQLStory>> = [
export const stories = createFixtures<GQLStory>([
{
id: "story-1",
closedAt: null,
@@ -430,17 +511,17 @@ export const stories: Array<Partial<GQLStory>> = [
publishedAt: "2018-11-29T16:01:51.897Z",
},
},
];
]);
export const storyConnection = {
export const storyConnection = createFixture<GQLStoriesConnection>({
edges: [
{ node: stories[0], cursor: stories[0].createdAt },
{ node: stories[1], cursor: stories[1].createdAt },
],
pageInfo: { endCursor: null, hasNextPage: false },
};
});
export const emptyStories = {
export const emptyStories = createFixture<GQLStoriesConnection>({
edges: [],
pageInfo: { endCursor: null, hasNextPage: false },
};
});
@@ -157,7 +157,7 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -310,7 +310,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -450,7 +450,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Lukas
Ngoc
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -597,7 +597,7 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
Max
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -766,7 +766,7 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -922,7 +922,7 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -1090,7 +1090,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -1230,7 +1230,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Lukas
Ngoc
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -1376,7 +1376,7 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
Max
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -1537,7 +1537,7 @@ exports[`single comment view accepts single comment 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -1680,7 +1680,7 @@ exports[`single comment view rejects single comment 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -1858,7 +1858,7 @@ exports[`single comment view renders single comment view 1`] = `
<span
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Markus
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
@@ -44,7 +44,7 @@ const createTestRenderer = async (resolver: any = {}) => {
comments:
get(resolver, "Query.comments") ||
sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
@@ -36,7 +36,7 @@ const createTestRenderer = async (resolver: any = {}) => {
expectAndFail(data.status).toBeFalsy();
return storyConnection;
}),
viewer: sinon.stub().returns(users[0]),
viewer: sinon.stub().returns(users.admins[0]),
...resolver.Query,
},
};