diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.ts deleted file mode 100644 index e456dce3b1f7..000000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2022 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - customFormatDateTime, - getEpochMillisForFutureDays, -} from '../../../src/utils/date-time/DateTimeUtils'; -import { - descriptionBox, - interceptURL, - uuid, - verifyResponseStatusCode, -} from '../../common/common'; -import { DELETE_TERM } from '../../constants/constants'; -import { GlobalSettingOptions } from '../../constants/settings.constant'; - -const botName = `bot-ct-test-${uuid()}`; -const botEmail = `${botName}@mail.com`; -const description = 'This is bot description'; -const updatedDescription = 'This is updated bot description'; -const updatedBotName = `updated-${botName}`; -const unlimitedExpiryTime = 'This token has no expiration date.'; -const JWTToken = 'OpenMetadata JWT'; - -const expirationTime = { - oneday: '1', - sevendays: '7', - onemonth: '30', - twomonths: '60', - threemonths: '90', -}; -const getCreatedBot = () => { - interceptURL('GET', `/api/v1/bots/name/${botName}*`, 'getCreatedBot'); - // Click on created Bot name - cy.get(`[data-testid="bot-link-${botName}"]`).should('exist').click(); - verifyResponseStatusCode('@getCreatedBot', 200); -}; - -const revokeToken = () => { - // Click on revoke button - cy.get('[data-testid="revoke-button"]').click(); - // Verify the revoke text - cy.get('[data-testid="body-text"]').should( - 'contain', - 'Are you sure you want to revoke access for JWT Token?' - ); - interceptURL('PUT', `/api/v1/users/revokeToken`, 'revokeToken'); - // Click on confirm button - cy.get('[data-testid="save-button"]').click(); - verifyResponseStatusCode('@revokeToken', 200); - - // Verify the revoke is successful - cy.get('[data-testid="revoke-button"]').should('not.exist'); - cy.get('[data-testid="auth-mechanism"]') - .should('be.visible') - .invoke('text') - .should('eq', 'OpenMetadata JWT'); - cy.get('[data-testid="token-expiry"]').should('exist').should('be.visible'); - cy.get('[data-testid="save-edit"]').should('exist').should('be.visible'); -}; - -describe('Bots Page should work properly', { tags: 'Settings' }, () => { - beforeEach(() => { - cy.login(); - interceptURL( - 'GET', - 'api/v1/bots?limit=*&include=non-deleted', - 'getBotsList' - ); - cy.settingClick(GlobalSettingOptions.BOTS); - verifyResponseStatusCode('@getBotsList', 200); - }); - - it('Verify ingestion bot delete button is always disabled', () => { - cy.get('[data-testid="bot-delete-ingestion-bot"]') - .should('exist') - .should('be.disabled'); - }); - - it('Create new Bot', () => { - cy.get('[data-testid="add-bot"]') - .should('exist') - .should('be.visible') - .as('addBotButton'); - cy.wait(500); - // Click on add bot button - cy.get('@addBotButton').click(); - // Enter email - cy.get('[data-testid="email"]').should('exist').type(botEmail); - // Enter display name - cy.get('[data-testid="displayName"]').should('exist').type(botName); - // Select expiry time - cy.get('[data-testid="token-expiry"]').should('be.visible').click(); - cy.contains('1 hr').should('exist').should('be.visible').click(); - // Enter description - cy.get(descriptionBox).type(description); - // Click on save button - cy.wait(1000); - interceptURL('post', '/api/v1/bots', 'createBot'); - cy.get('[data-testid="save-user"]') - .scrollIntoView() - .should('be.visible') - .click(); - verifyResponseStatusCode('@createBot', 201); - verifyResponseStatusCode('@getBotsList', 200); - // Verify bot is getting added in the bots listing page - cy.get('table').should('contain', botName).and('contain', description); - - getCreatedBot(); - cy.get('[data-testid="revoke-button"]') - .should('be.visible') - .should('contain', 'Revoke token'); - - cy.get('[data-testid="center-panel"]') - .should('be.visible') - .should('contain', `${JWTToken} Token`); - // Verify expiration time - cy.get('[data-testid="token-expiry"]').should('be.visible'); - }); - - Object.values(expirationTime).forEach((expiry) => { - it(`Update token expiration for ${expiry} days`, () => { - getCreatedBot(); - revokeToken(); - // Click on dropdown - cy.get('[data-testid="token-expiry"]').click(); - // Select the expiration period - cy.contains(`${expiry} days`).should('exist').click(); - // Save the updated date - const expiryDate = customFormatDateTime( - getEpochMillisForFutureDays(expiry), - `ccc d'th' MMMM, yyyy` - ); - cy.get('[data-testid="save-edit"]').should('be.visible').click(); - cy.get('[data-testid="center-panel"]') - .find('[data-testid="revoke-button"]') - .should('be.visible'); - // Verify the expiry time - cy.get('[data-testid="token-expiry"]') - .should('be.visible') - .invoke('text') - .should('contain', `Expires on ${expiryDate}`); - }); - }); - - it('Update token expiration for unlimited days', () => { - getCreatedBot(); - revokeToken(); - // Click on expiry token dropdown - cy.get('[data-testid="token-expiry"]').click(); - // Select unlimited days - cy.contains('Unlimited days').click(); - // Save the selected changes - cy.get('[data-testid="save-edit"]').click(); - // Verify the updated expiry time - cy.get('[data-testid="center-panel"]') - .find('[data-testid="revoke-button"]') - .should('be.visible'); - // Verify the expiry time - cy.get('[data-testid="token-expiry"]') - .should('be.visible') - .invoke('text') - .should('contain', `${unlimitedExpiryTime}`); - }); - - it('Update display name and description', () => { - getCreatedBot(); - - // Click on edit display name - cy.get('[data-testid="edit-displayName"]') - .should('exist') - .should('be.visible') - .click(); - // Enter new display name - cy.get('[data-testid="displayName"]') - .should('be.visible') - .clear() - .type(updatedBotName); - // Save the updated display name - - cy.get('[data-testid="save-displayName"]').should('be.visible').click(); - - // Verify the display name is updated on bot details page - cy.get('[data-testid="left-panel"]').should('contain', updatedBotName); - cy.wait(1000); - // Click on edit description button - cy.get('[data-testid="edit-description"]').should('be.visible').click(); - // Enter updated description and save - cy.get(descriptionBox).clear().type(updatedDescription); - cy.get('[data-testid="save"]').click(); - - interceptURL('GET', '/api/v1/bots*', 'getBotsPage'); - cy.get('[data-testid="breadcrumb-link"]').first().click(); - verifyResponseStatusCode('@getBotsPage', 200); - - // Verify the updated name is displayed in the Bots listing page - cy.get(`[data-testid="bot-link-${updatedBotName}"]`).should( - 'contain', - updatedBotName - ); - cy.get('[data-testid="markdown-parser"]').should( - 'contain', - updatedDescription - ); - }); - - it('Delete created bot', () => { - // Click on delete button - cy.get(`[data-testid="bot-delete-${botName}"]`) - .should('be.visible') - .click(); - // Select permanent delete - cy.get('[data-testid="hard-delete-option"]').should('be.visible').click(); - // Enter confirmation text - cy.get('[data-testid="confirmation-text-input"]') - .should('be.visible') - .type(DELETE_TERM); - interceptURL('DELETE', '/api/v1/bots/*', 'deleteBot'); - cy.get('[data-testid="confirm-button"]').should('be.visible').click(); - verifyResponseStatusCode('@deleteBot', 200); - cy.get('[data-testid="page-layout-v1"]').should('not.contain', botName); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts new file mode 100644 index 000000000000..8737a2c72948 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, Page, test as base } from '@playwright/test'; +import { GlobalSettingOptions } from '../../constant/settings'; +import { BotClass } from '../../support/bot/BotClass'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { + createBot, + deleteBot, + getCreatedBot, + tokenExpirationForDays, + tokenExpirationUnlimitedDays, + updateBotDetails, +} from '../../utils/bot'; +import { redirectToHomePage } from '../../utils/common'; +import { settingClick } from '../../utils/sidebar'; + +const adminUser = new UserClass(); +const bot = new BotClass(); + +const test = base.extend<{ adminPage: Page }>({ + adminPage: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + await adminPage.close(); + }, +}); + +test.describe('Bots Page should work properly', () => { + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await bot.create(apiContext); + + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.delete(apiContext); + await bot.delete(apiContext); + + await afterAction(); + }); + + test('Verify ingestion bot delete button is always disabled', async ({ + adminPage, + }) => { + await redirectToHomePage(adminPage); + await settingClick(adminPage, GlobalSettingOptions.BOTS); + + await expect( + adminPage.getByTestId('bot-delete-ingestion-bot') + ).toBeDisabled(); + }); + + test('Create and Delete Bot', async ({ adminPage }) => { + await redirectToHomePage(adminPage); + await settingClick(adminPage, GlobalSettingOptions.BOTS); + await createBot(adminPage); + await deleteBot(adminPage); + }); + + test('Update display name and description', async ({ adminPage }) => { + await redirectToHomePage(adminPage); + await settingClick(adminPage, GlobalSettingOptions.BOTS); + await updateBotDetails(adminPage, bot.responseData); + }); + + test('Update token expiration', async ({ adminPage }) => { + test.slow(true); + + await redirectToHomePage(adminPage); + await settingClick(adminPage, GlobalSettingOptions.BOTS); + await getCreatedBot(adminPage, bot.responseData.name); + await tokenExpirationForDays(adminPage); + await tokenExpirationUnlimitedDays(adminPage); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/bot/BotClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/bot/BotClass.ts new file mode 100644 index 000000000000..ae4cbb7fc5ce --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/bot/BotClass.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { APIRequestContext } from '@playwright/test'; +import { uuid } from '../../utils/common'; + +export type BotResponseDataType = { + name: string; + botUser: string; + description: string; + id?: string; + fullyQualifiedName?: string; +}; + +export type UserResponseDataType = { + botUser: undefined; + name: string; + description: string; + id?: string; + fullyQualifiedName?: string; + email: string; + isAdmin: boolean; + isBot: boolean; + authenticationMechanism: { + authType: string; + config: { + JWTTokenExpiry: string; + }; + }; +}; + +export class BotClass { + id = uuid(); + data: BotResponseDataType; + userData: UserResponseDataType; + responseData: BotResponseDataType; + + constructor(data?: BotResponseDataType) { + this.data = data ?? { + botUser: `PW%Bot-${this.id}`, + name: `PW%Bot-${this.id}`, + description: 'playwright for bot description', + }; + this.userData = { + ...this.data, + botUser: undefined, + email: `pw_bot${this.id}@gmail.com`, + isAdmin: false, + isBot: true, + authenticationMechanism: { + authType: 'JWT', + config: { + JWTTokenExpiry: 'OneHour', + }, + }, + }; + } + + get() { + return this.responseData; + } + + async create(apiContext: APIRequestContext) { + const userResponse = await apiContext.put('/api/v1/users', { + data: this.userData, + }); + + const response = await apiContext.post('/api/v1/bots', { + data: this.data, + }); + const data = await response.json(); + this.responseData = data; + + const userResponseData = await userResponse.json(); + this.userData = userResponseData; + + return data; + } + + async delete(apiContext: APIRequestContext) { + const response = await apiContext.delete( + `/api/v1/bots/${this.responseData.id}?hardDelete=true&recursive=false` + ); + + return await response.json(); + } + + async patch(apiContext: APIRequestContext, data: Record[]) { + const response = await apiContext.patch( + `/api/v1/bots/${this.responseData.id}`, + { + data, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + + this.responseData = await response.json(); + + return await response.json(); + } +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts new file mode 100644 index 000000000000..b2fc24c34f1d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -0,0 +1,210 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, Page } from '@playwright/test'; +import { + customFormatDateTime, + getEpochMillisForFutureDays, +} from '../../src/utils/date-time/DateTimeUtils'; +import { GlobalSettingOptions } from '../constant/settings'; +import { BotResponseDataType } from '../support/bot/BotClass'; +import { descriptionBox, toastNotification, uuid } from './common'; +import { settingClick } from './sidebar'; +import { revokeToken } from './user'; + +const botName = `bot-ct-test-${uuid()}`; + +const BOT_DETAILS = { + botName: botName, + botEmail: `${botName}@mail.com`, + description: 'This is bot description', + updatedDescription: 'This is updated bot description', + updatedBotName: `updated-${botName}`, + unlimitedExpiryTime: 'This token has no expiration date.', + JWTToken: 'OpenMetadata JWT', +}; + +const EXPIRATION_TIME = [1, 7, 30, 60, 90]; + +export const getCreatedBot = async (page: Page, botName: string) => { + // Click on created Bot name + const fetchResponse = page.waitForResponse( + `/api/v1/bots/name/${encodeURIComponent(botName)}?*` + ); + await page.getByTestId(`bot-link-${botName}`).click(); + await fetchResponse; +}; + +export const createBot = async (page: Page) => { + // Click on add bot button + await page.getByTestId('add-bot').click(); + + // Fill the form details + await page.getByTestId('email').fill(BOT_DETAILS.botEmail); + + await page.getByTestId('displayName').fill(BOT_DETAILS.botName); + + // Select expiry time + await page.click('[data-testid="token-expiry"]'); + await page.locator('[title="1 hr"] div').click(); + + await page.locator(descriptionBox).fill(BOT_DETAILS.description); + + const saveResponse = page.waitForResponse('/api/v1/bots'); + await page.click('[data-testid="save-user"]'); + await saveResponse; + + // Verify bot is getting added in the bots listing page + const table = page.locator('table'); + + await expect(table).toContainText(BOT_DETAILS.botName); + await expect(table).toContainText(BOT_DETAILS.description); + + // Get created bot + await getCreatedBot(page, botName); // Replace with actual function to get created bot + + await expect(page.getByTestId('revoke-button')).toContainText('Revoke token'); + + await expect(page.getByTestId('center-panel')).toContainText( + `${BOT_DETAILS.JWTToken} Token` + ); + + await expect(page.getByTestId('token-expiry')).toBeVisible(); + + await toastNotification(page, 'Bot created successfully.'); +}; + +export const deleteBot = async (page: Page) => { + await settingClick(page, GlobalSettingOptions.BOTS); + + // Click on delete button + await page.getByTestId(`bot-delete-${botName}`).click(); + + await page.getByTestId('hard-delete-option').click(); + + await page.getByTestId('confirmation-text-input').fill('DELETE'); + + const deleteResponse = page.waitForResponse(`/api/v1/bots/*`); + + await page.getByTestId('confirm-button').click(); + + await deleteResponse; + + await toastNotification(page, /deleted successfully!/); + + await expect(page.getByTestId('page-layout-v1')).not.toContainText(botName); +}; + +export const updateBotDetails = async ( + page: Page, + botData: BotResponseDataType +) => { + await getCreatedBot(page, botData.name); + + await page.click('[data-testid="edit-displayName"]'); + await page.getByTestId('displayName').fill(BOT_DETAILS.updatedBotName); + + const updateDisplayNameResponse = page.waitForResponse( + `api/v1/bots/${botData.id ?? ''}` + ); + await page.getByTestId('save-displayName').click(); + await updateDisplayNameResponse; + + // Verify the display name is updated on bot details page + await expect( + page.locator('[data-testid="left-panel"] .display-name') + ).toContainText(BOT_DETAILS.updatedBotName); + + // Click on edit description button + await page.getByTestId('edit-description').click(); + await page.locator(descriptionBox).fill(BOT_DETAILS.updatedDescription); + + const updateDescriptionResponse = page.waitForResponse( + `api/v1/bots/${botData.id ?? ''}` + ); + await page.getByTestId('save').click(); + await updateDescriptionResponse; + + // Click on the breadcrumb link to go back to the bots listing page + const getBotsPageResponse = page.waitForResponse('/api/v1/bots*'); + await page.locator('[data-testid="breadcrumb-link"]').first().click(); + await getBotsPageResponse; + + // Verify the updated name is displayed in the Bots listing page + await expect( + page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) + ).toContainText(BOT_DETAILS.updatedBotName); + + await expect( + page.locator( + `[data-row-key="${botData.name}"] [data-testid="markdown-parser"]` + ) + ).toContainText(BOT_DETAILS.updatedDescription); +}; + +export const tokenExpirationForDays = async (page: Page) => { + for (const expiryTime of EXPIRATION_TIME) { + await revokeToken(page, true); + + // Click on dropdown + await page.click('[data-testid="token-expiry"]'); + + // Select the expiration period + await page.locator(`text=${expiryTime} days`).click(); + + // Save the updated date + const expiryDate = customFormatDateTime( + getEpochMillisForFutureDays(expiryTime), + `ccc d'th' MMMM, yyyy` + ); + + await page.click('[data-testid="save-edit"]'); + + await expect( + page.locator('[data-testid="center-panel"] [data-testid="revoke-button"]') + ).toBeVisible(); + + // Verify the expiry time + const tokenExpiryText = await page + .locator('[data-testid="token-expiry"]') + .innerText(); + + expect(tokenExpiryText).toContain(`Expires on ${expiryDate}`); + } +}; + +export const tokenExpirationUnlimitedDays = async (page: Page) => { + await revokeToken(page, true); + + // Click on expiry token dropdown + await page.click('[data-testid="token-expiry"]'); + // Select unlimited days + await page.getByText('Unlimited days').click(); + // Save the selected changes + await page.click('[data-testid="save-edit"]'); + + // Verify the updated expiry time + const revokeButton = page.locator( + '[data-testid="center-panel"] [data-testid="revoke-button"]' + ); + + await expect(revokeButton).toBeVisible(); + + // Verify the expiry time + const tokenExpiry = page.locator('[data-testid="token-expiry"]'); + + await expect(tokenExpiry).toBeVisible(); + + const tokenExpiryText = await tokenExpiry.innerText(); + + expect(tokenExpiryText).toContain(BOT_DETAILS.unlimitedExpiryTime); +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index 36c2f20c0c73..846d50030557 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -469,11 +469,13 @@ export const generateToken = async (page: Page) => { await generateToken; }; -export const revokeToken = async (page: Page) => { +export const revokeToken = async (page: Page, isBot?: boolean) => { await page.click('[data-testid="revoke-button"]'); await expect(page.locator('[data-testid="body-text"]')).toContainText( - 'Are you sure you want to revoke access for Personal Access Token?' + `Are you sure you want to revoke access for ${ + isBot ? 'JWT Token' : 'Personal Access Token' + }?` ); await page.click('[data-testid="save-button"]');