From e5d80e23522e5a6a7f21bb35d0800bda022b7c6e Mon Sep 17 00:00:00 2001 From: astandrik Date: Sat, 28 Dec 2024 14:38:28 +0300 Subject: [PATCH 1/9] chore: acl tests --- tests/suites/tenant/summary/ObjectSummary.ts | 46 +++++++++++++++++++ .../tenant/summary/objectSummary.test.ts | 39 ++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/tests/suites/tenant/summary/ObjectSummary.ts b/tests/suites/tenant/summary/ObjectSummary.ts index 09df3ad09..07fd3526e 100644 --- a/tests/suites/tenant/summary/ObjectSummary.ts +++ b/tests/suites/tenant/summary/ObjectSummary.ts @@ -18,6 +18,9 @@ export class ObjectSummary { private treeRows: Locator; private primaryKeys: Locator; private actionsMenu: ActionsMenu; + private aclWrapper: Locator; + private aclList: Locator; + private effectiveAclList: Locator; constructor(page: Page) { this.tree = page.locator('.ydb-object-summary__tree'); @@ -26,6 +29,49 @@ export class ObjectSummary { this.schemaViewer = page.locator('.schema-viewer'); this.primaryKeys = page.locator('.schema-viewer__keys_type_primary'); this.actionsMenu = new ActionsMenu(page.locator('.g-popup.g-popup_open')); + this.aclWrapper = page.locator('.ydb-acl'); + this.aclList = this.aclWrapper.locator('dl.gc-definition-list').first(); + this.effectiveAclList = this.aclWrapper.locator('dl.gc-definition-list').last(); + } + + async waitForAclVisible() { + await this.aclWrapper.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async getAccessRights(): Promise<{user: string; rights: string}[]> { + await this.waitForAclVisible(); + const items = await this.aclList.locator('.gc-definition-list__item').all(); + const result = []; + + for (const item of items) { + const user = + (await item.locator('.gc-definition-list__term-wrapper span').textContent()) || ''; + const definitionContent = await item.locator('.gc-definition-list__definition').first(); + const rights = (await definitionContent.textContent()) || ''; + result.push({user: user.trim(), rights: rights.trim()}); + } + + return result; + } + + async getEffectiveAccessRights(): Promise<{group: string; permissions: string[]}[]> { + await this.waitForAclVisible(); + const items = await this.effectiveAclList.locator('.gc-definition-list__item').all(); + const result = []; + + for (const item of items) { + const group = + (await item.locator('.gc-definition-list__term-wrapper span').textContent()) || ''; + const definitionContent = await item.locator('.gc-definition-list__definition').first(); + const permissionElements = await definitionContent.locator('span').all(); + const permissions = await Promise.all( + permissionElements.map(async (el) => ((await el.textContent()) || '').trim()), + ); + result.push({group: group.trim(), permissions}); + } + + return result; } async isTreeVisible() { diff --git a/tests/suites/tenant/summary/objectSummary.test.ts b/tests/suites/tenant/summary/objectSummary.test.ts index 438df5e17..b5501d7e0 100644 --- a/tests/suites/tenant/summary/objectSummary.test.ts +++ b/tests/suites/tenant/summary/objectSummary.test.ts @@ -160,4 +160,43 @@ test.describe('Object Summary', async () => { // Verify the column lists are different expect(vslotsColumns).not.toEqual(storagePoolsColumns); }); + + test.only('ACL tab shows correct access rights', async ({page}) => { + const pageQueryParams = { + schema: '/local/.sys_health', + database: '/local', + summaryTab: 'acl', + tenantPage: 'query', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const objectSummary = new ObjectSummary(page); + await objectSummary.waitForAclVisible(); + + // Check Access Rights + const accessRights = await objectSummary.getAccessRights(); + expect(accessRights).toEqual([{user: 'root@builtin', rights: 'Owner'}]); + + // Check Effective Access Rights + const effectiveRights = await objectSummary.getEffectiveAccessRights(); + expect(effectiveRights).toEqual([ + {group: 'USERS', permissions: ['ConnectDatabase']}, + {group: 'METADATA-READERS', permissions: ['List']}, + {group: 'DATA-READERS', permissions: ['SelectRow']}, + {group: 'DATA-WRITERS', permissions: ['UpdateRow', 'EraseRow']}, + { + group: 'DDL-ADMINS', + permissions: [ + 'WriteAttributes', + 'CreateDirectory', + 'CreateTable', + 'RemoveSchema', + 'AlterSchema', + ], + }, + {group: 'ACCESS-ADMINS', permissions: ['GrantAccessRights']}, + {group: 'DATABASE-ADMINS', permissions: ['Manage']}, + ]); + }); }); From e958f2943e53588e3bedc2400fdd2ccdc541c123 Mon Sep 17 00:00:00 2001 From: astandrik Date: Sat, 28 Dec 2024 15:10:23 +0300 Subject: [PATCH 2/9] chore: copy path --- .../tenant/summary/objectSummary.test.ts | 21 ++++++++++++++++++- tests/utils/clipboard.ts | 15 +++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/utils/clipboard.ts diff --git a/tests/suites/tenant/summary/objectSummary.test.ts b/tests/suites/tenant/summary/objectSummary.test.ts index b5501d7e0..12a8728c3 100644 --- a/tests/suites/tenant/summary/objectSummary.test.ts +++ b/tests/suites/tenant/summary/objectSummary.test.ts @@ -1,6 +1,7 @@ import {expect, test} from '@playwright/test'; import {wait} from '../../../../src/utils'; +import {getClipboardContent} from '../../../utils/clipboard'; import { backend, dsStoragePoolsTableName, @@ -161,7 +162,7 @@ test.describe('Object Summary', async () => { expect(vslotsColumns).not.toEqual(storagePoolsColumns); }); - test.only('ACL tab shows correct access rights', async ({page}) => { + test('ACL tab shows correct access rights', async ({page}) => { const pageQueryParams = { schema: '/local/.sys_health', database: '/local', @@ -199,4 +200,22 @@ test.describe('Object Summary', async () => { {group: 'DATABASE-ADMINS', permissions: ['Manage']}, ]); }); + + test('Copy path copies correct path to clipboard', async ({page}) => { + const pageQueryParams = { + schema: dsVslotsSchema, + database: tenantName, + general: 'query', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const objectSummary = new ObjectSummary(page); + await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.CopyPath); + + await page.waitForTimeout(100); + + const clipboardContent = await getClipboardContent(page); + expect(clipboardContent).toBe('.sys/ds_vslots'); + }); }); diff --git a/tests/utils/clipboard.ts b/tests/utils/clipboard.ts new file mode 100644 index 000000000..1b28a43e6 --- /dev/null +++ b/tests/utils/clipboard.ts @@ -0,0 +1,15 @@ +import type {Page} from '@playwright/test'; + +export const getClipboardContent = async (page: Page): Promise => { + await page.context().grantPermissions(['clipboard-read']); + + return page.evaluate(async () => { + try { + const text = await navigator.clipboard.readText(); + return text; + } catch (error) { + console.error('Failed to read clipboard:', error); + return ''; + } + }); +}; From 02e1e08aa42fb16fb004d018500f36c26b257382 Mon Sep 17 00:00:00 2001 From: astandrik Date: Sat, 28 Dec 2024 15:21:50 +0300 Subject: [PATCH 3/9] chore: create directory test --- tests/suites/tenant/summary/ObjectSummary.ts | 35 +++++++++++++++++++ .../tenant/summary/objectSummary.test.ts | 26 ++++++++++++++ tests/suites/tenant/summary/types.ts | 1 + 3 files changed, 62 insertions(+) diff --git a/tests/suites/tenant/summary/ObjectSummary.ts b/tests/suites/tenant/summary/ObjectSummary.ts index 07fd3526e..dfb0c4f3c 100644 --- a/tests/suites/tenant/summary/ObjectSummary.ts +++ b/tests/suites/tenant/summary/ObjectSummary.ts @@ -21,6 +21,9 @@ export class ObjectSummary { private aclWrapper: Locator; private aclList: Locator; private effectiveAclList: Locator; + private createDirectoryModal: Locator; + private createDirectoryInput: Locator; + private createDirectoryButton: Locator; constructor(page: Page) { this.tree = page.locator('.ydb-object-summary__tree'); @@ -32,6 +35,38 @@ export class ObjectSummary { this.aclWrapper = page.locator('.ydb-acl'); this.aclList = this.aclWrapper.locator('dl.gc-definition-list').first(); this.effectiveAclList = this.aclWrapper.locator('dl.gc-definition-list').last(); + this.createDirectoryModal = page.locator('.g-modal.g-modal_open'); + this.createDirectoryInput = page.locator( + '.g-text-input__control[placeholder="Relative path"]', + ); + this.createDirectoryButton = page.locator('button.g-button_view_action:has-text("Create")'); + } + + async isCreateDirectoryModalVisible(): Promise { + try { + await this.createDirectoryModal.waitFor({ + state: 'visible', + timeout: VISIBILITY_TIMEOUT, + }); + return true; + } catch (error) { + return false; + } + } + + async enterDirectoryName(name: string): Promise { + await this.createDirectoryInput.fill(name); + } + + async clickCreateDirectoryButton(): Promise { + await this.createDirectoryButton.click(); + } + + async createDirectory(name: string): Promise { + await this.enterDirectoryName(name); + await this.clickCreateDirectoryButton(); + // Wait for modal to close + await this.createDirectoryModal.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT}); } async waitForAclVisible() { diff --git a/tests/suites/tenant/summary/objectSummary.test.ts b/tests/suites/tenant/summary/objectSummary.test.ts index 12a8728c3..5578fb2f2 100644 --- a/tests/suites/tenant/summary/objectSummary.test.ts +++ b/tests/suites/tenant/summary/objectSummary.test.ts @@ -218,4 +218,30 @@ test.describe('Object Summary', async () => { const clipboardContent = await getClipboardContent(page); expect(clipboardContent).toBe('.sys/ds_vslots'); }); + + test('Create directory in local node', async ({page}) => { + const pageQueryParams = { + schema: tenantName, + database: tenantName, + general: 'query', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const objectSummary = new ObjectSummary(page); + await expect(objectSummary.isTreeVisible()).resolves.toBe(true); + + const directoryName = `test_dir_${Date.now()}`; + + // Open actions menu and click Create directory + await objectSummary.clickActionMenuItem('local', RowTableAction.CreateDirectory); + await expect(objectSummary.isCreateDirectoryModalVisible()).resolves.toBe(true); + + // Create directory + await objectSummary.createDirectory(directoryName); + + // Verify the new directory appears in the tree + const treeItem = page.locator('.ydb-tree-view').filter({hasText: directoryName}); + await expect(treeItem).toBeVisible(); + }); }); diff --git a/tests/suites/tenant/summary/types.ts b/tests/suites/tenant/summary/types.ts index f0e074d08..50f872c84 100644 --- a/tests/suites/tenant/summary/types.ts +++ b/tests/suites/tenant/summary/types.ts @@ -6,4 +6,5 @@ export enum RowTableAction { UpsertQuery = 'Upsert query...', AddIndex = 'Add index...', CreateChangefeed = 'Create changefeed...', + CreateDirectory = 'Create directory', } From 04032d40e73dac2d8decf2b21bb581d35ef97d89 Mon Sep 17 00:00:00 2001 From: astandrik Date: Sat, 28 Dec 2024 15:30:26 +0300 Subject: [PATCH 4/9] fix: increase timeout --- tests/suites/tenant/summary/objectSummary.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/suites/tenant/summary/objectSummary.test.ts b/tests/suites/tenant/summary/objectSummary.test.ts index 5578fb2f2..1ba0544b6 100644 --- a/tests/suites/tenant/summary/objectSummary.test.ts +++ b/tests/suites/tenant/summary/objectSummary.test.ts @@ -213,7 +213,7 @@ test.describe('Object Summary', async () => { const objectSummary = new ObjectSummary(page); await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.CopyPath); - await page.waitForTimeout(100); + await page.waitForTimeout(1000); const clipboardContent = await getClipboardContent(page); expect(clipboardContent).toBe('.sys/ds_vslots'); From cb7bace37e82dd5d8d07ad614b848bfc3eb2936e Mon Sep 17 00:00:00 2001 From: astandrik Date: Sat, 28 Dec 2024 16:13:37 +0300 Subject: [PATCH 5/9] chore: refresh button --- .../SchemaTree/RefreshTreeButton.tsx | 2 ++ tests/suites/tenant/summary/ObjectSummary.ts | 8 +++-- .../tenant/summary/objectSummary.test.ts | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/RefreshTreeButton.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/RefreshTreeButton.tsx index daab9e38f..f69bf8e34 100644 --- a/src/containers/Tenant/ObjectSummary/SchemaTree/RefreshTreeButton.tsx +++ b/src/containers/Tenant/ObjectSummary/SchemaTree/RefreshTreeButton.tsx @@ -3,12 +3,14 @@ import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit'; import {nanoid} from '@reduxjs/toolkit'; import {useDispatchTreeKey} from '../UpdateTreeContext'; +import {b} from '../shared'; export function RefreshTreeButton() { const updateTreeKey = useDispatchTreeKey(); return (