Files
enterprise-addons/knowledge/static/tests/permission_panel.test.js
T

763 lines
27 KiB
JavaScript

import { PermissionPanel } from "@knowledge/components/permission_panel/permission_panel";
import { knowledgeTopbar } from "@knowledge/components/topbar/topbar";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import {
defineActions,
defineModels,
fields,
makeMockEnv,
makeMockServer,
mockService,
models,
mountView,
patchWithCleanup,
serverState,
} from "@web/../tests/web_test_helpers";
import { user } from "@web/core/user";
import { beforeEach, expect, test } from "@odoo/hoot";
import { click, queryText } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mockKnowledgePermissionPanelRpc } from "./knowledge_test_helpers";
const permissions = {
none: "No Access",
read: "Can Read",
write: "Can Edit",
};
const internalPermissions = {
none: "Members only",
read: "Can Read",
write: "Can Edit",
};
const permLvl = {
none: 0,
read: 1,
write: 2,
};
let members = [];
class KnowledgeArticle extends models.ServerModel {
_name = "knowledge.article";
name = fields.Char();
active = fields.Boolean({ default: true });
icon = fields.Char();
parent_path = fields.Char();
parent_id = fields.Many2one({ relation: "knowledge.article" });
root_article_id = fields.Many2one({ relation: "knowledge.article" });
category = fields.Selection({ selection: ["private", "workspace", "shared"] });
is_article_visible_by_everyone = fields.Boolean();
is_desynchronized = fields.Boolean();
internal_permission = fields.Selection({ selection: Object.keys(internalPermissions) });
inherited_permission_parent_id = fields.Many2one({ relation: "knowledge.article" });
inherited_permission = fields.Selection({ selection: Object.keys(internalPermissions) });
user_can_write = fields.Boolean({ compute: "_compute_user_can_write" });
user_permission = fields.Selection({ selection: Object.keys(permissions) });
_records = [
{
id: 1,
name: "Private Article",
icon: "⛔️",
parent_id: false,
category: "private",
is_article_visible_by_everyone: false,
is_desynchronized: false,
internal_permission: "none",
inherited_permission_parent_id: false,
inherited_permission: "none",
user_permission: "write",
},
{
id: 2,
name: "Readonly Article",
icon: "🚷",
parent_id: false,
category: "workspace",
is_article_visible_by_everyone: false,
is_desynchronized: false,
internal_permission: "read",
inherited_permission_parent_id: false,
inherited_permission: "read",
user_permission: "read",
},
{
id: 3,
name: "Shared Article",
icon: "👥",
parent_id: false,
category: "shared",
is_article_visible_by_everyone: false,
is_desynchronized: false,
internal_permission: "none",
inherited_permission_parent_id: false,
inherited_permission: "none",
user_permission: "write",
},
{
id: 4,
name: "Workspace Article",
icon: "👨‍💻",
parent_id: false,
category: "workspace",
is_article_visible_by_everyone: true,
is_desynchronized: false,
internal_permission: "write",
inherited_permission_parent_id: false,
inherited_permission: "write",
user_permission: "write",
},
{
id: 5,
name: "Workspace Child Article",
icon: "",
parent_id: 4,
category: "workspace",
is_article_visible_by_everyone: true,
is_desynchronized: false,
internal_permission: false,
inherited_permission_parent_id: 4,
inherited_permission: "write",
user_permission: "write",
},
{
id: 6,
name: "Desynced Child Article",
icon: "",
parent_id: 4,
category: "workspace",
is_article_visible_by_everyone: true,
is_desynchronized: true,
internal_permission: "read",
inherited_permission_parent_id: false,
inherited_permission: "read",
user_permission: "write",
},
];
_views = {
form: '<form><widget name="knowledge_topbar"/></form>',
};
_compute_display_name() {
for (const record of this) {
record.display_name = record.icon + record.name;
}
}
async _compute_user_can_write() {
for (const record of this) {
record.user_can_write =
user.isAdmin ||
record.user_permission === "write" ||
members.some(
(member) =>
member.partner_id === serverState.partnerId && member.permission === "write"
);
}
}
get_permission_panel_members(id) {
// should define members array in each test as members model is not defined for simplicity
return members;
}
restore_article_access(id) {
this.write(id, {
is_desynchronized: false,
inherited_permission_parent_id: 4,
internal_permission: false,
inherited_permission: "write",
});
}
set_internal_permission(id, permission) {
this.write(id, { internal_permission: permission, inherited_permission: permission });
}
set_is_article_visible_by_everyone(id, visibility) {
this.write(id, { is_article_visible_by_everyone: visibility });
}
set_member_permission(id, memberId, permission) {
const member = members.find((member) => member.member_id === memberId);
if (!member) {
return;
}
if (member.based_on && permLvl[permission] < permLvl[member.permission]) {
this.write(id, { is_desynchronized: true });
Object.assign(member, { based_on: false, based_on_icon: false, based_on_name: false });
}
Object.assign(member, { permission });
}
remove_member(id, memberId) {
const index = members.findIndex((member) => member.member_id === memberId);
if (index !== -1) {
// test cases removing (non) inherited member
if (id === 5) {
if (members[index].based_on) {
this.write(id, { is_desynchronized: true });
} else {
Object.assign(members[index], {
based_on: true,
based_on_name: "Workspace Article",
based_on_icon: "👨‍💻",
permission: "write",
});
return;
}
}
members.splice(index, 1);
}
}
action_send_to_trash() {
expect.step("trash");
}
action_redirect_to_parent() {
return { action: "redirect to parent" };
}
has_access = function () {
return Promise.resolve(true);
};
}
defineMailModels();
defineModels([KnowledgeArticle]);
// dummy action to mock the invite dialog
defineActions([
{
id: 99,
xml_id: "knowledge.knowledge_invite_action_from_article",
name: "Invite",
res_model: "knowledge.article",
target: "new",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
]);
const internalPermissionSelector = ".o_internal_permission .dropdown-toggle";
const internalVisibilitySelector = ".o_internal_visibility .dropdown-toggle";
const inviteMemberSelector = ".fa-user-plus + .input-group";
const expectMember = (member, disabled) => {
const selector = `.o_knowledge_permission_panel_members > div:contains(${member.name})`;
const texts = [member.name];
if (member.partner_share) {
texts.push(member.partner_id === serverState.partnerId ? "(You)" : "(Guest)");
}
texts.push(member.email);
if (member.based_on) {
texts.push(`Based on ${member.based_on_icon} ${member.based_on_name}`);
}
texts.push(permissions[member.permission]);
expect(queryText(selector).split("\n")).toEqual(texts);
if (disabled) {
expect(selector + " .dropdown-toggle").toHaveClass("disabled");
} else {
expect(selector + " .dropdown-toggle").not.toHaveClass("disabled");
}
};
patchWithCleanup(knowledgeTopbar, {
fieldDependencies: Object.entries(KnowledgeArticle._fields).map(([fieldName, field]) => ({
name: fieldName,
type: field.type,
})),
});
let mockServer;
beforeEach(async () => {
mockServer = await makeMockServer();
await makeMockEnv({
/** @param {integer} articleId */
async openArticle(articleId) {
expect.step(`open ${articleId}`);
await this.reactiveRecordWrapper.record.model.load({ resId: articleId });
},
sendArticleToTrash() {
expect.step("send to trash");
},
save: () => {},
discard: () => {},
commentsState: { isDisplayed: false },
chatterPanelState: { isDisplayed: false },
propertiesPanelState: { isDisplayed: true },
});
patchWithCleanup(PermissionPanel.prototype, {
load() {
expect.step("reload record");
return super.load();
},
});
});
async function mountPermissionPanel(articleId, user_can_write = undefined) {
if (user_can_write !== undefined) {
mockServer.env["knowledge.article"].write(articleId, { user_can_write });
}
await mountView({
resId: articleId,
resModel: "knowledge.article",
type: "form",
arch: KnowledgeArticle._views.form,
});
await click("button[data-hotkey='shift+s']");
await animationFrame();
}
mockKnowledgePermissionPanelRpc();
test.tags("desktop");
test("Permission Panel as a read only user", async () => {
patchWithCleanup(user, { isAdmin: false }); // readonly internal user
members = [
{
member_id: 1,
name: "current",
email: "current@example.com",
permission: "read",
partner_id: serverState.partnerId,
partner_share: true,
},
{
member_id: 2,
name: "alice",
email: "alice@example.com",
permission: "write",
},
];
await mountPermissionPanel(2, false);
await animationFrame();
// internal permission, visibility and members should appear as disabled
expect(internalPermissionSelector).toHaveText("Can Read");
expect(internalPermissionSelector).toHaveClass("disabled");
expect(internalVisibilitySelector).toHaveText("Members only");
expect(internalVisibilitySelector).toHaveClass("disabled");
// since there is no right escalation in this case, the user can remove its member.
expectMember(members[0], false);
// invite input should not be shown
expect(inviteMemberSelector).toHaveCount(0);
});
test.tags("desktop");
test("Permission Panel as a portal user", async () => {
patchWithCleanup(user, { isAdmin: false, hasGroup: (group) => group !== "base.group_user" }); // portal user
members = [
{
member_id: 1,
name: "portal",
email: "portal@example.com",
partner_share: true,
based_on: true,
based_on_name: "Parent Article",
based_on_icon: "👨‍💻",
permission: "write",
},
{
member_id: 2,
name: "shrek",
email: "shrek@example.com",
permission: "write",
},
];
await mountPermissionPanel(5, true);
await animationFrame();
// internal permission panel is not shown
expect(internalPermissionSelector).toHaveCount(0);
expect(internalVisibilitySelector).toHaveCount(0);
// invitation input is not shown
expect(inviteMemberSelector).toHaveCount(0);
// members are shown with a disabled permission selector
expect(".o_knowledge_permission_panel_members > div").toHaveCount(2);
expectMember(members[0], true);
expectMember(members[1], true);
});
test.tags("desktop");
test("Add a read member on a private article", async () => {
members = [
{
member_id: 1,
name: "user1",
email: "user1@example.com",
permission: "write",
},
];
await mountPermissionPanel(1, true);
expect(internalPermissionSelector).toHaveText("Members only");
expect(internalVisibilitySelector).toHaveCount(0);
expect(".o_knowledge_permission_panel_members > div").toHaveCount(1);
// member should be disabled: admin can't remove the editor of another users' private article)
expectMember(members[0], true);
await click(inviteMemberSelector);
members.push({
member_id: 2,
name: "invited user",
email: "invited@example.com",
permission: "read",
});
await animationFrame();
await click(".modal-header .btn-close");
await animationFrame();
// internal visibility should remain hidden as article is shared
expect(internalVisibilitySelector).toHaveCount(0);
// new member should be shown with the right permission
expectMember(members[1]);
// writer should not be able to change its permission as a writer is always required on an article
expectMember(members[0], true);
expect.verifySteps(["reload record"]);
});
test.tags("desktop");
test("Remove a member", async () => {
members = [
{
member_id: 1,
name: "bob",
email: "bob@example.com",
permission: "write",
},
{
member_id: 2,
name: "alice",
email: "alice@example.com",
permission: "write",
},
];
await mountPermissionPanel(3, true);
await animationFrame();
expect(".o_knowledge_permission_panel_members > div").toHaveCount(2);
await click(".o_knowledge_permission_panel_members > div:first-child .dropdown-toggle");
await animationFrame();
expect(".o_knowledge_permission_panel_remove_member").toHaveText("Remove");
await click(".o_knowledge_permission_panel_remove_member");
await animationFrame();
expect(".o_knowledge_permission_panel_members > div").toHaveCount(1);
expect.verifySteps(["remove member 2 on article 3", "reload record"]);
});
test.tags("desktop");
test("Leave a private article", async () => {
members = [
{
member_id: 1,
name: "admin",
email: "admin@example.com",
permission: "write",
partner_id: serverState.partnerId,
partner_share: true,
},
];
await mountPermissionPanel(1, true);
await animationFrame();
await click(".o_knowledge_permission_panel_members .dropdown-toggle");
await animationFrame();
expect(".o_knowledge_permission_panel_remove_member").toHaveCount(1);
expect(".o_knowledge_permission_panel_remove_member").toHaveText("Leave");
await click(".o_knowledge_permission_panel_remove_member");
await animationFrame();
expect(".modal-title").toHaveText("Leave Private Article");
await click(".modal-footer button:contains('Move to Trash')");
await animationFrame();
expect.verifySteps(["send to trash"]);
});
test.tags("desktop");
test("Change internal permission and visibility", async () => {
members = [];
await mountPermissionPanel(1, true);
await animationFrame();
expect(internalPermissionSelector).toHaveText("Members only");
expect(internalVisibilitySelector).toHaveCount(0);
await click(internalPermissionSelector);
await animationFrame();
await click(".o-dropdown-item:contains('Can Edit')");
await animationFrame();
expect(internalPermissionSelector).toHaveText("Can Edit");
expect(internalVisibilitySelector).toHaveText("Members only");
expect.verifySteps(["change permission to write on article 1", "reload record"]);
await click(internalVisibilitySelector);
await animationFrame();
await click(".o-dropdown-item:contains('Everyone')");
await animationFrame();
expect(internalVisibilitySelector).toHaveText("Everyone");
expect.verifySteps(["change visibility to everyone on article 1", "reload record"]);
});
test.tags("desktop");
test("Change inherited internal permission", async () => {
members = [];
await mountPermissionPanel(5, true);
await animationFrame();
expect(internalPermissionSelector).toHaveText("Can Edit");
// child articles do not have the visibility option
expect(internalVisibilitySelector).toHaveCount(0);
expect(".knowledge_internal_permission_based_on_text").toHaveText(
"Based on 👨‍💻Workspace Article"
);
await click(internalPermissionSelector);
await animationFrame();
await click(".o-dropdown-item:contains('Members only')");
await animationFrame();
// downgrading internal permission should show confirmation dialog
expect(".modal-title").toHaveText("Restrict Access");
await click(".modal-footer button:contains('Restrict access')");
await animationFrame();
expect(internalPermissionSelector).toHaveText("Members only");
expect.verifySteps(["change permission to none on article 5", "reload record"]);
// for simplicity the test model does not desync the article from its parent when changing the
// permission, so we can check the behavior when upgrading the permission of a synced article
// in the same test
await click(internalPermissionSelector);
await animationFrame();
await click(".o-dropdown-item:contains('Can Read')");
// upgrading internal permission of synced article should not show confirmation modal
await animationFrame();
expect(".modal-title").toHaveCount(0);
expect(internalPermissionSelector).toHaveText("Can Read");
expect.verifySteps(["change permission to read on article 5", "reload record"]);
});
test.tags("desktop");
test("Downgrade inherited member permission", async () => {
members = [
{
member_id: 1,
name: "inherited",
email: "inherited@example.com",
based_on: true,
based_on_name: "Parent Article",
based_on_icon: "👨‍💻",
permission: "write",
},
];
await mountPermissionPanel(5, true);
await animationFrame();
expectMember(members[0]);
await click(".o_knowledge_permission_panel_members > div:first-child .dropdown-toggle");
await animationFrame();
await click(".o-dropdown-item:contains('Can Read')");
await animationFrame();
// should show confirmation dialog as it will restrict user's access
expect(".modal-title").toHaveText("Change Permission");
await click(".modal-footer button:contains('Restrict Access')");
await animationFrame();
expectMember(members[0]);
// article should now appear as desync
expect("div:contains('Access Restricted') + button:contains('Restore')").toHaveCount(1);
expect("div:contains('Access Restricted') > a").toHaveText("👨‍💻Workspace Article");
expect.verifySteps(["change permission of member 1 to read on article 5", "reload record"]);
});
test.tags("desktop");
test("Upgrade inherited member permission", async () => {
members = [
{
member_id: 1,
name: "inherited",
email: "inherited@example.com",
based_on: true,
based_on_name: "Parent Article",
based_on_icon: "👨‍💻",
permission: "read",
},
];
await mountPermissionPanel(5, true);
await animationFrame();
expectMember(members[0]);
await click(".o_knowledge_permission_panel_members > div:first-child .dropdown-toggle");
await animationFrame();
await click(".o-dropdown-item:contains('Can Edit')");
await animationFrame();
// since we are modifying the perm from its parent we need to confirm
expect(".modal").toHaveCount(1);
await click(".modal-footer button:contains('Restrict Access')");
await animationFrame();
// member should not appear as "based on" anymore
expectMember(members[0]);
// article should not appear as desync
expect("div:contains('Access Restricted') + button:contains('Restore')").toHaveCount(0);
expect.verifySteps(["change permission of member 1 to write on article 5", "reload record"]);
});
test.tags("desktop");
test("Remove inherited member", async () => {
members = [
{
member_id: 1,
name: "inherited",
email: "inherited@example.com",
based_on: true,
based_on_name: "Parent Article",
based_on_icon: "👨‍💻",
permission: "read",
},
];
await mountPermissionPanel(5, true);
await animationFrame();
expectMember(members[0]);
await click(".o_knowledge_permission_panel_members .dropdown-toggle");
await animationFrame();
await click(".o_knowledge_permission_panel_remove_member");
await animationFrame();
// should show confirmation dialog as it will restrict user's access
expect(".modal-title").toHaveText("Restrict Access");
await click(".modal-footer button:contains('Restrict Access')");
await animationFrame();
expect(".o_knowledge_permission_panel_members > div").toHaveCount(0);
// article should now appear as desync
expect("div:contains('Access Restricted') + button:contains('Restore')").toHaveCount(1);
expect("div:contains('Access Restricted') > a").toHaveText("👨‍💻Workspace Article");
expect.verifySteps([`remove member 1 on article 5`, "reload record"]);
});
test.tags("desktop");
test("Can't remove an inherited member with permission = 'none'", async () => {
members = [
{
member_id: 1,
name: "inherited",
email: "inherited@example.com",
based_on: true,
based_on_name: "Parent Article",
based_on_icon: "👨‍💻",
permission: "none",
},
];
await mountPermissionPanel(5, true);
await animationFrame();
expectMember(members[0]);
expect(".o_select_menu_toggler_clear").toHaveCount(0);
});
test.tags("desktop");
test("Remove member while member has permission on a parent", async () => {
members = [
{
member_id: 1,
name: "member",
email: "member@example.com",
based_on: false,
permission: "read",
},
];
await mountPermissionPanel(5, true);
await animationFrame();
expectMember(members[0]);
await click(".o_knowledge_permission_panel_members .dropdown-toggle");
await animationFrame();
await click(".o_knowledge_permission_panel_remove_member");
await animationFrame();
// should not show modal
expect(".modal-title").toHaveCount(0);
// check that the inherited member is now shown
expectMember({
member_id: 1,
name: "member",
email: "member@example.com",
based_on: true,
based_on_name: "Workspace Article",
based_on_icon: "👨‍💻",
permission: "write",
});
expect.verifySteps(["remove member 1 on article 5", "reload record"]);
});
test.tags("desktop");
test("Change article while permission panel is open", async () => {
await mountPermissionPanel(6, true);
await animationFrame();
expect(internalPermissionSelector).toHaveText("Can Read");
await click(".fa-shield + div a:contains('👨‍💻Workspace Article')");
await animationFrame();
expect.verifySteps(["open 4"]);
// check that panel has been reloaded
expect(internalPermissionSelector).toHaveText("Can Edit");
});
test.tags("desktop");
test("Downgrade own permission", async () => {
members = [
{
member_id: 1,
name: "current",
email: "current@example.com",
permission: "write",
partner_id: serverState.partnerId,
partner_share: true,
},
];
patchWithCleanup(user, { isAdmin: false }); // not admin user
mockService("action", {
doAction: (action) => {
if (action === "knowledge.ir_actions_server_knowledge_home_page") {
expect.step("home");
}
},
});
await mountPermissionPanel(4, true);
await animationFrame();
expectMember(members[0]);
await click(".o_knowledge_permission_panel_members > div:first-child .dropdown-toggle");
await animationFrame();
await click(".o-dropdown-item:contains('No Access')");
await animationFrame();
// should show confirmation dialog as it will restrict user's access
expect(".modal-title").toHaveText("Leave Article");
await click(".modal-footer button:contains('Lose Access')");
await animationFrame();
expect.verifySteps([
"change permission of member 1 to none on article 4",
"reload record",
"home",
]);
});
test.tags("desktop");
test("Restore Article", async () => {
members = [
{
member_id: 1,
name: "internal",
email: "internal@example.com",
permission: "write",
},
];
await mountPermissionPanel(6, true);
await animationFrame();
expect(internalPermissionSelector).toHaveText("Can Read");
// "based on" should not be visible
expect(".knowledge_internal_permission_based_on_text").toHaveCount(0);
// member's dropdown is disabled since its the only editor
expectMember(members[0], true);
// should show "restricted" message and "restore" button
expect("div:contains('Access Restricted') + button:contains('Restore')").toHaveCount(1);
expect("div:contains('Access Restricted') > a").toHaveText("👨‍💻Workspace Article");
// update members list to check that restore will refetch members
Object.assign(members[0], {
based_on: 4,
based_on_name: "Workspace Article",
based_on_icon: "👨‍💻",
});
await click("button:contains('Restore')");
await animationFrame();
expect(".modal .btn-primary:contains('Restore Access')").toHaveCount(1);
await click(".modal .btn-primary:contains('Restore Access')");
await animationFrame();
// check permission panel is updated and is now "based on"
expect(internalPermissionSelector).toHaveText("Can Edit");
expect(".knowledge_internal_permission_based_on_text").toHaveCount(1);
expectMember(members[0]);
// should not show "restricted" message and "restore" button anymore
expect("div:contains('Access Restricted') + button:contains('Restore')").toHaveCount(0);
expect.verifySteps(["restoring access on article 6", "reload record"]);
});