diff --git a/e2etests/api-requests/restapi-request.ts b/e2etests/api-requests/restapi-request.ts index 446038668..1cd086562 100644 --- a/e2etests/api-requests/restapi-request.ts +++ b/e2etests/api-requests/restapi-request.ts @@ -11,6 +11,7 @@ export class RestAPIRequest extends BaseRequest { readonly employeesEndpoint: string; readonly organizationConstraintsEndpoint: string; readonly policiesEndpoint: string; + readonly taggingPoliciesEndpoint: string; readonly poolsEndpoint: string; /** @@ -25,6 +26,7 @@ export class RestAPIRequest extends BaseRequest { this.employeesEndpoint = `${baseUrl}/restapi/v2/employees`; this.organizationConstraintsEndpoint = `${baseUrl}/restapi/v2/organization_constraints`; this.policiesEndpoint = `${this.organizationsEndpoint}/${process.env.DEFAULT_ORG_ID}/organization_constraints?hit_days=3&type=resource_quota&type=recurring_budget&type=expiring_budget`; + this.taggingPoliciesEndpoint = `${this.organizationsEndpoint}/${process.env.DEFAULT_ORG_ID}/organization_constraints?hit_days=3&type=tagging_policy`; this.poolsEndpoint = `${baseUrl}/restapi/v2/pools`; } @@ -107,14 +109,13 @@ export class RestAPIRequest extends BaseRequest { */ async deletePolicy(policyID: string, token: string): Promise { const endpoint = `${this.organizationConstraintsEndpoint}/${policyID}`; - debugLog(`Deleting anomaly policy ${policyID}`); const response = await this.request.delete(endpoint, { headers: getBearerTokenHeader(token), }); if (response.status() !== 204) { throw new Error(`[ERROR] Failed to delete anomaly policy ID: ${policyID}`); } - debugLog(`Anomaly policy ${policyID} deleted`); + debugLog(`Policy ${policyID} deleted`); } /** @@ -131,6 +132,14 @@ export class RestAPIRequest extends BaseRequest { }); } + async getTaggingPolicies(token: string): Promise { + const endpoint = this.taggingPoliciesEndpoint; + + return await this.request.get(endpoint, { + headers: getBearerTokenHeader(token), + }); + } + async getPoolsResponse(token: string): Promise { const endpoint = `${this.poolsEndpoint}/${getEnvironmentParentPoolId()}?children=true&details=true`; const headers = getBearerTokenHeader(token); diff --git a/e2etests/mocks/tagging-policy-page.mocks.ts b/e2etests/mocks/tagging-policy-page.mocks.ts new file mode 100644 index 000000000..fb8ca91d4 --- /dev/null +++ b/e2etests/mocks/tagging-policy-page.mocks.ts @@ -0,0 +1,135 @@ +export const TaggingPolicyViolationResponse = { + organization_constraints: [ + { + deleted_at: 0, + id: '1dc38049-b397-45ff-b786-6f42c6b60f12', + created_at: 1769696555, + name: 'Required Tag Policy 1769696547422', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'tagging_policy', + definition: { + start_date: 1769644800, + conditions: { + without_tag: 'AccountId', + }, + }, + filters: {}, + last_run: 1769696702, + last_run_result: { + value: 3185, + }, + limit_hits: [ + { + deleted_at: 0, + id: '6dea591a-ec7b-4eba-bbf0-4444e0773f4a', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '1dc38049-b397-45ff-b786-6f42c6b60f12', + constraint_limit: 0.0, + value: 3185.0, + created_at: 1769696702, + run_result: { + value: 3185, + }, + }, + ], + }, + { + deleted_at: 0, + id: '40c24616-6016-454e-be6f-ca4c165f8ceb', + created_at: 1769696597, + name: 'Correlated Tag Policy 1769696585306', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'tagging_policy', + definition: { + start_date: 1769644800, + conditions: { + tag: 'CostCenter', + without_tag: 'Environment', + }, + }, + filters: {}, + last_run: 1769696702, + last_run_result: { + value: 1, + }, + limit_hits: [ + { + deleted_at: 0, + id: '8032d27f-81c3-4eaa-b0c3-791d53d8ae36', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '40c24616-6016-454e-be6f-ca4c165f8ceb', + constraint_limit: 0.0, + value: 1.0, + created_at: 1769696702, + run_result: { + value: 1, + }, + }, + ], + }, + { + deleted_at: 0, + id: '62f013ef-748e-4d06-860b-3f530b5c685f', + created_at: 1769696578, + name: 'Prohibited Tag Policy 1769696563199', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'tagging_policy', + definition: { + start_date: 1769644800, + conditions: { + tag: '__department', + }, + }, + filters: { + active: [true], + }, + last_run: 1769696702, + last_run_result: { + value: 2, + }, + limit_hits: [ + { + deleted_at: 0, + id: '77a91b69-d183-4f46-9b5e-d1173e0fe031', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '62f013ef-748e-4d06-860b-3f530b5c685f', + constraint_limit: 0.0, + value: 2.0, + created_at: 1769696702, + run_result: { + value: 2, + }, + }, + ], + }, + { + "deleted_at": 0, + "id": "e26bca14-4a38-49dc-a9cc-0798406cc6e4", + "created_at": 1769697639, + "name": "Non-violating Correlated Tag Policy", + "organization_id": "4eae08f8-9b40-4094-a11c-f9ee2dc76a12", + "type": "tagging_policy", + "definition": { + "start_date": 1769697608, + "conditions": { + "tag": "CostCenter", + "without_tag": "Environment" + } + }, + "filters": { + "cloud_account": [ + { + "id": "ec8b9ca5-6d16-465e-8831-472a6bcd5fcf", + "name": "Marketplace (Dev)", + "type": "aws_cnr" + } + ] + }, + "last_run": 1769697902, + "last_run_result": { + "value": 0 + }, + "limit_hits": [] + } + ], +}; diff --git a/e2etests/pages/base-create-page.ts b/e2etests/pages/base-create-page.ts index 1400695cd..b9c7d5b43 100644 --- a/e2etests/pages/base-create-page.ts +++ b/e2etests/pages/base-create-page.ts @@ -42,6 +42,12 @@ export abstract class BaseCreatePage extends BasePage { readonly saveBtn: Locator; readonly cancelBtn: Locator; + readonly setDateBtn: Locator; + readonly timePicker: Locator; + readonly amButton: Locator; + readonly pmButton: Locator; + readonly setButton: Locator; + /** * Initializes a new instance of the BaseCreatePage class. * @param {Page} page - The Playwright page object. @@ -57,7 +63,7 @@ export abstract class BaseCreatePage extends BasePage { this.filtersBox = this.main.locator('xpath=(//div[.="Filters:"])[1]/..'); this.allFilterBoxButtons = this.filtersBox.locator('button'); this.filterPopover = this.page.locator('//div[contains(@id, "filter-popover")]'); - this.filterApplyButton = this.filterPopover.getByRole('button' , { name: 'Apply' }); + this.filterApplyButton = this.filterPopover.getByRole('button', { name: 'Apply' }); this.suggestionsFilter = this.filtersBox.getByRole('button', { name: 'Suggestions' }); this.dataSourceFilter = this.filtersBox.getByRole('button', { name: 'Data source (' }); @@ -84,5 +90,66 @@ export abstract class BaseCreatePage extends BasePage { this.showLessFiltersBtn = this.main.getByRole('button', { name: 'Show less' }); this.saveBtn = this.main.getByTestId('btn_create'); this.cancelBtn = this.main.getByTestId('btn_cancel'); + + this.setDateBtn = this.main.getByTestId('btn_select_date'); + this.timePicker = this.page.locator('//input[@data-test-id="half-hour-time-selector"]/..'); + this.amButton = this.page.getByRole('button', { name: 'AM' }); + this.pmButton = this.page.getByRole('button', { name: 'PM' }); + this.setButton = this.page.getByRole('button', { name: 'Set' }); + } + + /** + * Sets the time for the policy. + * + * @param {string} [time='12:00'] - The time to set in the format 'hh:mm'. + * @param {boolean} [am=true] - Whether to set the time as AM (true) or PM (false). + * @returns {Promise} A promise that resolves when the time is set. + */ + protected async setTime(time: string = '12:00', am: boolean = true): Promise { + await this.setDateBtn.click(); + await this.selectFromComboBox(this.timePicker, time); + if (am) { + await this.amButton.click(); + } else { + await this.pmButton.click(); + } + await this.setButton.click(); + } + + /** + * Selects a filter and applies the specified filter option. + * + * @param {Locator} filter - The filter locator to select. + * @param {string} filterOption - The specific filter option to apply. + * @throws {Error} Throws an error if `filterOption` is not provided when `filter` is specified. + * @returns {Promise} A promise that resolves when the filter is applied. + */ + protected async selectFilter(filter: Locator, filterOption: string): Promise { + if (filter) { + if (!filterOption) { + throw new Error('filterOption must be provided when filter is specified'); + } + if (!(await filter.isVisible())) await this.showMoreFiltersBtn.click(); + await filter.click(); + + await this.filterPopover.getByLabel(filterOption).click(); + await this.filterApplyButton.click(); + } + } + + /** + * Selects a filter by its text and applies the specified filter option. + * + * This method locates a filter button within the filters box by matching its name + * with the provided `filter` parameter. It then applies the specified `filterOption` + * to the selected filter. + * + * @param {string} filter - The name of the filter to select. + * @param {string} filterOption - The specific filter option to apply. + * @returns {Promise} A promise that resolves when the filter is applied. + */ + async selectFilterByText(filter: string, filterOption: string): Promise { + const filterLocator = this.filtersBox.getByRole('button', { name: new RegExp(`^${filter}`) }); + await this.selectFilter(filterLocator, filterOption); } } diff --git a/e2etests/pages/base-page.ts b/e2etests/pages/base-page.ts index 7093fe702..bef17cc4d 100644 --- a/e2etests/pages/base-page.ts +++ b/e2etests/pages/base-page.ts @@ -15,7 +15,6 @@ export abstract class BasePage { readonly progressBar: Locator; readonly tooltip: Locator; readonly table: Locator; - readonly infoColor: string; // Default color for neutral state readonly warningColor: string; // Default color for warning state readonly errorColor: string; // Default color for error state @@ -30,6 +29,7 @@ export abstract class BasePage { this.page = page; this.url = url; this.main = this.page.locator('main'); + this.table = this.main.locator('table'); this.loadingPageImg = this.page.getByRole('img', { name: 'Loading page' }); this.progressBar = this.page.locator('//main[@id="mainLayoutWrapper"]//*[@role="progressbar"]'); this.tooltip = this.page.getByRole('tooltip'); @@ -37,7 +37,6 @@ export abstract class BasePage { this.warningColor = 'rgb(232, 125, 30)'; // Default color for warning state this.errorColor = 'rgb(187, 20, 37)'; // Default color for error state this.successColor = 'rgb(0, 120, 77)'; // Default color for success state - this.table = this.main.locator('table'); } /** diff --git a/e2etests/pages/date-picker-page.ts b/e2etests/pages/date-picker-page.ts index ac1ebda07..0ae7b11e5 100644 --- a/e2etests/pages/date-picker-page.ts +++ b/e2etests/pages/date-picker-page.ts @@ -61,7 +61,7 @@ export class DatePickerPage extends BasePage { * @param {boolean} [wait=true] - Whether to wait for the page loader to disappear and the canvas to load after applying the date range. * @returns {Promise} A promise that resolves when the operation is complete. */ - async selectLast7DaysDateRange(wait = true): Promise { + async selectLast7DaysDateRange(wait: boolean = true): Promise { await this.selectDateBtn.click(); await this.last7DaysBtn.click(); await this.applyDateBtn.click(); @@ -78,7 +78,7 @@ export class DatePickerPage extends BasePage { * @param {boolean} [wait=true] - Whether to wait for the page loader to disappear and the canvas to load after applying the date range. * @returns {Promise} A promise that resolves when the operation is complete. */ - async selectLast30DaysDateRange(wait = true): Promise { + async selectLast30DaysDateRange(wait: boolean = true): Promise { await this.selectDateBtn.click(); await this.last30DaysBtn.click(); await this.applyDateBtn.click(); @@ -95,7 +95,7 @@ export class DatePickerPage extends BasePage { * @param {boolean} [wait=true] - Whether to wait for the page loader to disappear and the canvas to load after applying the date range. * @returns {Promise} A promise that resolves when the operation is complete. */ - async selectLastMonthDateRange(wait = true): Promise { + async selectLastMonthDateRange(wait: boolean = true): Promise { await this.selectDateBtn.click(); await this.lastMonthBtn.click(); await this.applyDateBtn.click(); @@ -112,7 +112,7 @@ export class DatePickerPage extends BasePage { * @param {boolean} [wait=true] - Whether to wait for the page loader to disappear and the canvas to load after applying the date range. * @returns {Promise} A promise that resolves when the operation is complete. */ - async selectThisMonthDateRange(wait = true): Promise { + async selectThisMonthDateRange(wait: boolean = true): Promise { await this.selectDateBtn.click(); await this.thisMonthBtn.click(); await this.applyDateBtn.click(); diff --git a/e2etests/pages/policies-create-page.ts b/e2etests/pages/policies-create-page.ts index c121d4e89..73e39eec2 100644 --- a/e2etests/pages/policies-create-page.ts +++ b/e2etests/pages/policies-create-page.ts @@ -10,11 +10,6 @@ export class PoliciesCreatePage extends BaseCreatePage { readonly resourceCountInput: Locator; readonly monthlyBudgetInput: Locator; readonly totalBudgetInput: Locator; - readonly setDateBtn: Locator; - readonly timePicker: Locator; - readonly amButton: Locator; - readonly pmButton: Locator; - readonly setButton: Locator; /** * Initializes a new instance of the PoliciesCreatePage class. @@ -26,11 +21,6 @@ export class PoliciesCreatePage extends BaseCreatePage { this.resourceCountInput = this.main.getByTestId('input_maxValue'); this.monthlyBudgetInput = this.main.getByTestId('input_monthlyBudget'); this.totalBudgetInput = this.main.getByTestId('input_totalBudget'); - this.setDateBtn = this.main.getByTestId('btn_select_date'); - this.timePicker = this.page.locator('//input[@data-test-id="half-hour-time-selector"]/..'); - this.amButton = this.page.getByRole('button', { name: 'AM' }); - this.pmButton = this.page.getByRole('button', { name: 'PM' }); - this.setButton = this.page.getByRole('button', { name: 'Set' }); } /** @@ -90,43 +80,4 @@ export class PoliciesCreatePage extends BaseCreatePage { await this.selectFilter(filter, filterOption); await this.saveBtn.click(); } - - /** - * Sets the time for the policy. - * - * @param {string} [time='12:00'] - The time to set in the format 'hh:mm'. - * @param {boolean} [am=true] - Whether to set the time as AM (true) or PM (false). - * @returns {Promise} A promise that resolves when the time is set. - */ - async setTime(time: string = '12:00', am: boolean = true): Promise { - await this.setDateBtn.click(); - await this.selectFromComboBox(this.timePicker, time); - if (am) { - await this.amButton.click(); - } else { - await this.pmButton.click(); - } - await this.setButton.click(); - } - - /** - * Selects a filter and applies the specified filter option. - * - * @param {Locator} filter - The filter locator to select. - * @param {string} filterOption - The specific filter option to apply. - * @throws {Error} Throws an error if `filterOption` is not provided when `filter` is specified. - * @returns {Promise} A promise that resolves when the filter is applied. - */ - private async selectFilter(filter: Locator, filterOption: string): Promise { - if (filter) { - if (!filterOption) { - throw new Error('filterOption must be provided when filter is specified'); - } - if (!(await filter.isVisible())) await this.showMoreFiltersBtn.click(); - await filter.click(); - - await this.filterPopover.getByLabel(filterOption).click(); - await this.filterApplyButton.click(); - } - } } diff --git a/e2etests/pages/tagging-policies-create-page.ts b/e2etests/pages/tagging-policies-create-page.ts index da09571a1..d6ac0d971 100644 --- a/e2etests/pages/tagging-policies-create-page.ts +++ b/e2etests/pages/tagging-policies-create-page.ts @@ -1,5 +1,7 @@ -import {BaseCreatePage} from "./base-create-page"; -import {Locator, Page} from "@playwright/test"; +import { BaseCreatePage } from './base-create-page'; +import { Locator, Page } from '@playwright/test'; +import { ETaggingPolicyType } from '../types/enums'; +import { debugLog } from '../utils/debug-logging'; /** * Represents the Tagging Policies Create Page. @@ -12,24 +14,102 @@ export class TaggingPoliciesCreatePage extends BaseCreatePage { readonly prohibitedTagsBtn: Locator; readonly tagsCorrelationTagsBtn: Locator; readonly requiredTagInput: Locator; + readonly requiredTagComboBox: Locator; readonly prohibitedTagInput: Locator; + readonly prohibitedTagComboBox: Locator; readonly primaryTagInput: Locator; + readonly primaryTagComboBox: Locator; readonly correlatedTagInput: Locator; + readonly correlatedTagComboBox: Locator; /** * Initializes a new instance of the TaggingPoliciesCreatePage class. * @param {Page} page - The Playwright page object. */ constructor(page: Page) { - super(page, "/tagging-policies/create"); + super(page, '/tagging-policies/create'); this.heading = this.main.getByTestId('lbl_create_tagging_policy'); this.startDateSelect = this.main.getByTestId('input_startDate'); this.requiredTagsBtn = this.main.getByTestId('tags_strategy_taggingPolicy.requiredTag'); this.prohibitedTagsBtn = this.main.getByTestId('tags_strategy_taggingPolicy.prohibitedTag'); this.tagsCorrelationTagsBtn = this.main.getByTestId('tags_strategy_taggingPolicy.tagsCorrelation'); this.requiredTagInput = this.main.getByTestId('input_requiredTagField'); + this.requiredTagComboBox = this.requiredTagInput.locator('xpath=..'); this.prohibitedTagInput = this.main.getByTestId('input_prohibitedTagField'); + this.prohibitedTagComboBox = this.prohibitedTagInput.locator('xpath=..'); this.primaryTagInput = this.main.getByTestId('input_tagsCorrelationPrimaryTag'); + this.primaryTagComboBox = this.primaryTagInput.locator('xpath=..'); this.correlatedTagInput = this.main.getByTestId('input_tagsCorrelationCorrelatedTag'); + this.correlatedTagComboBox = this.correlatedTagInput.locator('xpath=..'); + } + + /** + * Creates a tagging policy based on the specified type and parameters. + * + * This method handles the creation of tagging policies, including required tags, + * prohibited tags, and tags correlation. It performs the necessary actions + * based on the provided type, such as selecting buttons, waiting for progress + * bars to disappear, and filling in the required fields. If the `tagsCorrelation` + * type is selected, a secondary tag must be provided. Optionally, a filter can + * be applied to the tagging policy. + * + * @param {ETaggingPolicyType} type - The type of tagging policy to create. + * Possible values are: + * - `ETaggingPolicyType.requiredTag`: A policy for required tags. + * - `ETaggingPolicyType.prohibitedTag`: A policy for prohibited tags. + * - `ETaggingPolicyType.tagsCorrelation`: A policy for correlating tags. + * @param {string} name - The name of the tagging policy. + * @param {string} primaryTag - The primary tag to be used in the policy. + * @param {string} [secondaryTag] - The secondary tag, required for the + * `tagsCorrelation` type. + * @param {string} [filter] - The name of the filter to apply (optional). + * @param {string} [filterOption] - The specific filter option to apply (optional). + * @returns {Promise} A promise that resolves when the tagging policy is created. + * @throws {Error} If the `tagsCorrelation` type is selected and no secondary tag is provided. + * @throws {Error} If an unsupported tagging policy type is specified. + */ + async createTaggingPolicy( + type: ETaggingPolicyType, + name: string, + primaryTag: string, + secondaryTag?: string, + filter?: string, + filterOption?: string + ): Promise { + debugLog(`Creating tagging policy with the following parameters: + Type: ${type} + Name: ${name} + Primary Tag: ${primaryTag} + Secondary Tag: ${secondaryTag ?? 'N/A'} + Filter: ${filter ?? 'N/A'} + Filter Option: ${filterOption ?? 'N/A'}`); + await this.nameInput.fill(name); + await this.setTime(); + + switch (type) { + case ETaggingPolicyType.requiredTag: + await this.clickButtonIfNotActive(this.requiredTagsBtn); + await this.waitForAllProgressBarsToDisappear(); + await this.selectFromComboBox(this.requiredTagComboBox, primaryTag); + break; + case ETaggingPolicyType.prohibitedTag: + await this.clickButtonIfNotActive(this.prohibitedTagsBtn); + await this.waitForAllProgressBarsToDisappear(); + await this.selectFromComboBox(this.prohibitedTagComboBox, primaryTag); + break; + case ETaggingPolicyType.tagsCorrelation: + if (!secondaryTag) { + throw new Error('Secondary tag must be provided for Tags Correlation policy type.'); + } + await this.clickButtonIfNotActive(this.tagsCorrelationTagsBtn); + await this.waitForAllProgressBarsToDisappear(); + await this.selectFromComboBox(this.primaryTagComboBox, primaryTag); + await this.selectFromComboBox(this.correlatedTagComboBox, secondaryTag); + break; + default: + throw new Error(`Unsupported tagging policy type: ${type}`); + } + if (filter) await this.selectFilterByText(filter, filterOption); + await this.saveBtn.click(); } } diff --git a/e2etests/pages/tagging-policies-page.ts b/e2etests/pages/tagging-policies-page.ts index 147e06e40..4c3564a94 100644 --- a/e2etests/pages/tagging-policies-page.ts +++ b/e2etests/pages/tagging-policies-page.ts @@ -1,5 +1,5 @@ -import {Locator, Page} from "@playwright/test"; -import {BasePage} from "./base-page"; +import { Locator, Page } from '@playwright/test'; +import { BasePage } from './base-page'; /** * Represents the Tagging Policies Page. @@ -8,6 +8,10 @@ import {BasePage} from "./base-page"; export class TaggingPoliciesPage extends BasePage { readonly heading: Locator; readonly addBtn: Locator; + readonly addRealDataBtn: Locator; + readonly policyDetailsDiv: Locator; + readonly deleteBtn: Locator = this.main.getByTestId('btn_delete'); + readonly sideModalDeleteBtn: Locator = this.main.getByTestId('btn_smodal_delete'); /** * Initializes a new instance of the TaggingPoliciesPage class. @@ -17,6 +21,10 @@ export class TaggingPoliciesPage extends BasePage { super(page, '/tagging-policies'); this.heading = this.main.getByTestId('lbl_tagging_policies'); this.addBtn = this.main.getByTestId('btn_add'); + this.addRealDataBtn = this.getByAnyTestId('btn_add_tagging_policy'); + this.policyDetailsDiv = this.main.locator('//div[@class="MTPBoxShadow MuiBox-root mui-0"][1]'); + this.deleteBtn = this.page.getByTestId('btn_delete'); + this.sideModalDeleteBtn = this.page.getByTestId('btn_smodal_delete'); } /** @@ -26,4 +34,42 @@ export class TaggingPoliciesPage extends BasePage { async clickAddBtn(): Promise { await this.addBtn.click(); } + + /** + * Navigates to the "Create Tagging Policy" page. + * + * This method attempts to click the "Add Real Data" button if it is available within a timeout of 3 seconds. + * If the button is not available (e.g., real policies already exist), it falls back to clicking the "Add" button. + * After clicking the appropriate button, it waits for all progress bars to disappear, ensuring the navigation is complete. + * + * @returns {Promise} A promise that resolves when the navigation is complete. + */ + async navigateToCreateTaggingPolicy(): Promise { + try { + await this.addRealDataBtn.waitFor({ timeout: 3000 }); + await this.addRealDataBtn.click(); + await this.waitForAllProgressBarsToDisappear(); + return; + } catch { + // Do nothing if real policies exist + } + await this.clickAddBtn(); + await this.waitForAllProgressBarsToDisappear(); + } + + /** + * Deletes a tagging policy from the details page. + * + * This method performs the following steps: + * 1. Clicks the delete button on the policy details page. + * 2. Clicks the delete button in the side modal to confirm the deletion. + * 3. Waits for all progress bars to disappear, ensuring the deletion process is complete. + * + * @returns {Promise} A promise that resolves when the deletion process is complete. + */ + async deletePolicyFromDetailsPage(): Promise { + await this.deleteBtn.click(); + await this.sideModalDeleteBtn.click(); + await this.waitForAllProgressBarsToDisappear(); + } } diff --git a/e2etests/setup/global-teardown.ts b/e2etests/setup/global-teardown.ts index 239bade69..dc60d36f2 100644 --- a/e2etests/setup/global-teardown.ts +++ b/e2etests/setup/global-teardown.ts @@ -2,9 +2,11 @@ import { request } from '@playwright/test'; import { AuthRequest } from '../api-requests/auth-request'; import { RestAPIRequest } from '../api-requests/restapi-request'; import { - cleanUpDirectoryIfEnabled, connectDataSource, + cleanUpDirectoryIfEnabled, + connectDataSource, deletePolicies, deleteSubPoolsByName, + deleteTaggingPolicies, deleteTestUsers, disconnectDataSource, getDatasourceIdByNameViaOpsAPI, @@ -26,6 +28,7 @@ async function globalTeardown() { await cleanUpDirectoryIfEnabled('./tests/downloads'); await deleteTestUsers(restAPIRequest, token); await deletePolicies(restAPIRequest, token); + await deleteTaggingPolicies(restAPIRequest, token); // clear down orphaned Marketplace (Dev) Sub-pools and reconnect data source const dataSourceName = 'Marketplace (Dev)'; diff --git a/e2etests/tests/anomalies-tests.spec.ts b/e2etests/tests/anomalies-tests.spec.ts index 5fd1d22da..1d3108068 100644 --- a/e2etests/tests/anomalies-tests.spec.ts +++ b/e2etests/tests/anomalies-tests.spec.ts @@ -141,10 +141,7 @@ test.describe('[MPT-14737] Anomalies Tests', { tag: ['@ui', '@anomalies'] }, () const latestTimestamp = breakdownKeys[breakdownKeys.length - 1]; expect.soft(latestTimestamp - oldestTimestamp).toBe(6 * 86400); - // Validate timestamps are reasonable (within last 14 days for safety) const now = Math.floor(Date.now() / 1000); - const fifteenDaysAgo = now - 15 * 86400; - expect.soft(oldestTimestamp).toBeGreaterThan(fifteenDaysAgo); expect.soft(latestTimestamp).toBeLessThanOrEqual(now); // Validate each breakdown value is a number diff --git a/e2etests/tests/expenses-tests.spec.ts b/e2etests/tests/expenses-tests.spec.ts index e7769f567..a115252d2 100644 --- a/e2etests/tests/expenses-tests.spec.ts +++ b/e2etests/tests/expenses-tests.spec.ts @@ -288,7 +288,8 @@ test.describe('[MPT-12859] Expenses Page Source Breakdown Tests', { tag: ['@ui', await test.step('Compare total expenses values', async () => { const dateRange = await datePicker.selectedDateText.textContent(); - expect(dateRange.includes(expectedDateRange)).toBe(true); + debugLog(`Actual date range: ${dateRange}`); + expect.soft(dateRange.includes(expectedDateRange)).toBe(true); expect.soft(isWithinRoundingDrift(chartTotal, totalForPeriod, 0.001)).toBe(true); expect.soft(isWithinRoundingDrift(tableTotal, totalForPeriod, 0.001)).toBe(true); }); diff --git a/e2etests/tests/policies-tests.spec.ts b/e2etests/tests/policies-tests.spec.ts index d7faea3a6..62f1ec6ba 100644 --- a/e2etests/tests/policies-tests.spec.ts +++ b/e2etests/tests/policies-tests.spec.ts @@ -46,9 +46,12 @@ test.describe('[MPT-16366] Policies Tests', { tag: ['@ui', '@policies'] }, () => test('[232286] Verify that Sample data pop-up is visible when no policies exist', async ({ policiesPage }) => { await test.step('Ensure all policies are deleted', async () => { - await deleteAllPolicies(); - await policiesPage.page.reload(); - await policiesPage.waitForAllProgressBarsToDisappear(); + // eslint-disable-next-line playwright/no-conditional-in-test + if(!await policiesPage.realDataAddBtn.isVisible()) { + await deleteAllPolicies(); + await policiesPage.page.reload(); + await policiesPage.waitForAllProgressBarsToDisappear(); + } }); await test.step('Verify Sample data pop-up visibility', async () => { @@ -266,7 +269,7 @@ test.describe('[MPT-16366] Mocked Policies Tests', { tag: ['@ui', '@policies'] } test.beforeEach('Login admin user', async ({ policiesPage }) => { await test.step('Login admin user', async () => { - await policiesPage.page.clock.setFixedTime(new Date('2026-08-01T14:00:00Z')); + await policiesPage.page.clock.setFixedTime(new Date('2026-01-08T14:00:00Z')); await policiesPage.navigateToURL(); await policiesPage.waitForAllProgressBarsToDisappear(); }); diff --git a/e2etests/tests/tagging-policy-tests.spec.ts b/e2etests/tests/tagging-policy-tests.spec.ts new file mode 100644 index 000000000..1d2babb9d --- /dev/null +++ b/e2etests/tests/tagging-policy-tests.spec.ts @@ -0,0 +1,194 @@ +import { test } from '../fixtures/page.fixture'; +import { expect, request } from '@playwright/test'; +import { AuthRequest } from '../api-requests/auth-request'; +import { RestAPIRequest } from '../api-requests/restapi-request'; +import { deleteTaggingPolicies } from '../utils/teardown-utils'; +import { ETaggingPolicyType } from '../types/enums'; +import { InterceptionEntry } from '../types/interceptor.types'; +import { TaggingPolicyViolationResponse } from '../mocks/tagging-policy-page.mocks'; + +async function deleteAllPolicies() { + const apiContext = await request.newContext({ + ignoreHTTPSErrors: true, + baseURL: process.env.BASE_URL, + }); + const email = process.env.DEFAULT_USER_EMAIL; + const password = process.env.DEFAULT_USER_PASSWORD; + const authRequest = new AuthRequest(apiContext); + const restAPIRequest = new RestAPIRequest(apiContext); + const token = await authRequest.getAuthorizationToken(email, password); + await deleteTaggingPolicies(restAPIRequest, token); +} + +test.describe('[MPT-17042] Tagging Policy Tests', { tag: ['@ui', '@tagging-policies'] }, () => { + test.describe.configure({ mode: 'default' }); + test.use({ restoreSession: true }); + + test.beforeEach('Login admin user', async ({ taggingPoliciesPage }) => { + await test.step('Login admin user', async () => { + await taggingPoliciesPage.navigateToURL(); + await taggingPoliciesPage.waitForAllProgressBarsToDisappear(); + }); + }); + + test('[232655] Verify that Sample data pop-up is visible when no policies exist', async ({ taggingPoliciesPage }) => { + await test.step('Ensure all policies are deleted', async () => { + // eslint-disable-next-line playwright/no-conditional-in-test + if (!(await taggingPoliciesPage.addRealDataBtn.isVisible())) { + await deleteAllPolicies(); + await taggingPoliciesPage.page.reload(); + await taggingPoliciesPage.waitForAllProgressBarsToDisappear(); + } + }); + + await test.step('Verify Sample data pop-up visibility', async () => { + await expect(taggingPoliciesPage.addRealDataBtn).toBeVisible(); + }); + }); + + test('[232656] Verify that a user can create a required tagging policy', async ({ taggingPoliciesPage, taggingPoliciesCreatePage }) => { + const policyName = `Required Tag Policy ${Date.now()}`; + const tagName = 'AccountId'; + + await test.step('Create required Tagging Policy page', async () => { + await taggingPoliciesPage.navigateToCreateTaggingPolicy(); + await taggingPoliciesCreatePage.createTaggingPolicy(ETaggingPolicyType.requiredTag, policyName, tagName); + }); + + const targetPolicyRow = taggingPoliciesPage.table.locator(`//td[.="${policyName}"]/ancestor::tr`); + const date = `${(new Date().getMonth() + 1).toString().padStart(2, '0')}/${new Date().getDate().toString().padStart(2, '0')}/${new Date().getFullYear()} 12:00 AM`; + + await test.step('Verify that the required tagging policy is created', async () => { + await targetPolicyRow.waitFor(); + + await expect.soft(targetPolicyRow.locator('//td[1]')).toHaveText(policyName); + await expect.soft(targetPolicyRow.locator('//td[3]')).toHaveText(`The ${tagName} tag is required starting from ${date}.`); + }); + }); + + test('[232657] Verify that a user can create a prohibited tagging policy', async ({ taggingPoliciesPage, taggingPoliciesCreatePage }) => { + const policyName = `Prohibited Tag Policy ${Date.now()}`; + const tagName = '__department'; + const filter = 'Activity'; + const filterOption = 'Active'; + + await test.step('Create prohibited Tagging Policy page', async () => { + await taggingPoliciesPage.navigateToCreateTaggingPolicy(); + await taggingPoliciesCreatePage.createTaggingPolicy( + ETaggingPolicyType.prohibitedTag, + policyName, + tagName, + undefined, + filter, + filterOption + ); + }); + + const targetPolicyRow = taggingPoliciesPage.table.locator(`//td[.="${policyName}"]/ancestor::tr`); + const date = `${(new Date().getMonth() + 1).toString().padStart(2, '0')}/${new Date().getDate().toString().padStart(2, '0')}/${new Date().getFullYear()} 12:00 AM`; + + await test.step('Verify that the prohibited tagging policy is created', async () => { + await targetPolicyRow.waitFor(); + + await expect.soft(targetPolicyRow.locator('//td[1]')).toHaveText(policyName); + await expect.soft(targetPolicyRow.locator('//td[3]')).toHaveText(`Tag ${tagName} is prohibited starting from ${date}.`); + await expect(targetPolicyRow.locator('//td[4]')).toHaveText(`${filter}: ${filterOption}`); + }); + }); + + test('[232658] Verify that a user can create a tags correlation tagging policy', async ({ + taggingPoliciesPage, + taggingPoliciesCreatePage, + }) => { + const policyName = `Correlated Tag Policy ${Date.now()}`; + const tagName = 'CostCenter'; + const secondaryTagName = 'Environment'; + + await test.step('Create tags correlation Tagging Policy page', async () => { + await taggingPoliciesPage.navigateToCreateTaggingPolicy(); + await taggingPoliciesCreatePage.createTaggingPolicy(ETaggingPolicyType.tagsCorrelation, policyName, tagName, secondaryTagName); + }); + + const targetPolicyRow = taggingPoliciesPage.table.locator(`//td[.="${policyName}"]/ancestor::tr`); + const date = `${(new Date().getMonth() + 1).toString().padStart(2, '0')}/${new Date().getDate().toString().padStart(2, '0')}/${new Date().getFullYear()} 12:00 AM`; + + await test.step('Verify that the tags correlation tagging policy is created', async () => { + await targetPolicyRow.waitFor(); + + await expect.soft(targetPolicyRow.locator('//td[1]')).toHaveText(policyName); + await expect + .soft(targetPolicyRow.locator('//td[3]')) + .toHaveText(`Resources tagged with ${tagName} must be tagged with ${secondaryTagName} starting from ${date}.`); + }); + }); + + test('[232659] Verify that user can delete a policy from the tagging policy details page', async ({ + taggingPoliciesPage, + taggingPoliciesCreatePage, + }) => { + const policyName = `Policy To Be Deleted ${Date.now()}`; + const tagName = 'Application'; + + await test.step('Create a policy to be deleted', async () => { + await taggingPoliciesPage.navigateToCreateTaggingPolicy(); + await taggingPoliciesCreatePage.createTaggingPolicy(ETaggingPolicyType.requiredTag, policyName, tagName); + }); + + const targetPolicyRow = taggingPoliciesPage.table.locator(`//td[.="${policyName}"]/ancestor::tr`); + + await test.step('Navigate to the created policy details page', async () => { + await targetPolicyRow.waitFor(); + await taggingPoliciesPage.clickLocator(targetPolicyRow.locator('//a')); + await taggingPoliciesPage.policyDetailsDiv.waitFor(); + }); + + await test.step('Delete the policy from the details page', async () => { + await taggingPoliciesPage.deletePolicyFromDetailsPage(); + }); + + await test.step('Verify that the policy is deleted and no longer appears in the policies table', async () => { + await expect(targetPolicyRow).toBeHidden(); + }); + }); +}); + +test.describe('[MPT-17042] Mocked Tagging Policies Tests', { tag: ['@ui', '@tagging-policies'] }, () => { + test.describe.configure({ mode: 'default' }); + + const apiInterceptions: InterceptionEntry[] = [ + { + url: `v2/organizations/[^/]+/organization_constraints\\?hit_days=3&type=tagging_policy`, + mock: TaggingPolicyViolationResponse, + }, + ]; + + test.use({ + restoreSession: true, + interceptAPI: { entries: apiInterceptions, failOnInterceptionMissing: false }, + }); + + test.beforeEach('Login admin user', async ({ taggingPoliciesPage }) => { + await test.step('Login admin user', async () => { + await taggingPoliciesPage.page.clock.setFixedTime(new Date('2026-01-29T15:00:00Z')); + await taggingPoliciesPage.navigateToURL(); + await taggingPoliciesPage.waitForAllProgressBarsToDisappear(); + }); + }); + + test('[232660] Verify that tagging policies are displayed with the correct status', async ({ taggingPoliciesPage }) => { + const cancelIconXpath = '//*[@data-testid="CancelIcon"]'; + const checkCheckIconXpath = '//*[@data-testid="CheckCircleIcon"]'; + const correlatedTagStatus = taggingPoliciesPage.table.locator('(//a[contains(text(), "Correlated Tag")]/ancestor::tr/td[2]/div)[1]'); + const nonViolatingTagStatus = taggingPoliciesPage.table.locator('(//a[contains(text(), "Non-violating")]/ancestor::tr/td[2]/div)[1]'); + const prohibitedTagStatus = taggingPoliciesPage.table.locator('(//a[contains(text(), "Prohibited Tag")]/ancestor::tr/td[2]/div)[1]'); + const requiredTagStatus = taggingPoliciesPage.table.locator('(//a[contains(text(), "Required Tag")]/ancestor::tr/td[2]/div)[1]'); + + await expect.soft(correlatedTagStatus.locator(cancelIconXpath)).toBeVisible(); + await expect.soft(correlatedTagStatus).toHaveText('1 violation right now'); + await expect.soft(nonViolatingTagStatus.locator(checkCheckIconXpath)).toBeVisible(); + await expect.soft(prohibitedTagStatus.locator(cancelIconXpath)).toBeVisible(); + await expect.soft(prohibitedTagStatus).toHaveText('2 violations right now'); + await expect.soft(requiredTagStatus.locator(cancelIconXpath)).toBeVisible(); + await expect.soft(requiredTagStatus).toHaveText('3185 violations right now'); + }); +}); diff --git a/e2etests/types/api-response.types.ts b/e2etests/types/api-response.types.ts index 4133f3f07..ba1f4b7f7 100644 --- a/e2etests/types/api-response.types.ts +++ b/e2etests/types/api-response.types.ts @@ -572,6 +572,48 @@ export interface PolicyBudgetAndQuotaResponse { ]; } +export interface TaggingPolicyResponse { + organization_constraints: [ + { + deleted_at: number; + id: string; + created_at: number; + name: string; + organization_id: string; + type: 'tagging_policy'; + definition: { + start_date: number; + conditions: + | { + tag: string; + without_tag: string; + } + | { tag: string } + | { without_tag: string }; + }; + filters?: [Record]; + last_run: number; + last_run_result: { + value: number; + }; + limit_hits: [ + { + deleted_at: number; + id: string; + organization_id: string; + constraint_id: string; + constraint_limit: number; + value: number; + created_at: number; + run_result: { + value: number; + }; + }, + ]; + }, + ]; +} + /** * Definition differs by policy `type` * - recurring_budget → { monthly_budget: number } diff --git a/e2etests/types/enums.ts b/e2etests/types/enums.ts index 96e0eada6..7d67e9438 100644 --- a/e2etests/types/enums.ts +++ b/e2etests/types/enums.ts @@ -37,4 +37,10 @@ export enum EParentPoolId { DEV = "ccaceadf-6878-4ab4-9fd8-3f6177d0b9d3", STAGING = "624abd3c-0d70-4859-964a-e14aafb96c7b", TEST = "f648bd92-b53e-4fa7-aebb-cb02bcbf160d" +} + +export enum ETaggingPolicyType { + requiredTag = "Required tag", + prohibitedTag = "Prohibited tag", + tagsCorrelation = "Tags correlation" } \ No newline at end of file diff --git a/e2etests/utils/date-range-utils.ts b/e2etests/utils/date-range-utils.ts index f1d814c11..b89e06c54 100644 --- a/e2etests/utils/date-range-utils.ts +++ b/e2etests/utils/date-range-utils.ts @@ -52,7 +52,8 @@ export function getExpectedDateRangeText(rangeType: string, today: Date = new Da throw new Error(`Unsupported range type: ${rangeType}`); } - const yearsDiffer = startDate.getFullYear() !== endDate.getFullYear(); + const yearsDiffer = startDate.getUTCFullYear() !== endDate.getUTCFullYear() || + startDate.getUTCFullYear() !== today.getUTCFullYear(); const format = (date: Date) => date.toLocaleDateString('en-US', { diff --git a/e2etests/utils/teardown-utils.ts b/e2etests/utils/teardown-utils.ts index d62169715..9a1616a6f 100644 --- a/e2etests/utils/teardown-utils.ts +++ b/e2etests/utils/teardown-utils.ts @@ -1,7 +1,12 @@ import fs from 'fs'; import path from 'path'; import { debugLog } from './debug-logging'; -import { EmployeesResponse, PolicyBudgetAndQuotaResponse, PoolsResponse } from '../types/api-response.types'; +import { + EmployeesResponse, + PolicyBudgetAndQuotaResponse, + PoolsResponse, + TaggingPolicyResponse +} from '../types/api-response.types'; import { AuthRequest } from './api-requests/auth-request'; import { RestAPIRequest } from '../api-requests/restapi-request'; import { GetDatasourcesByOrganizationIDResponse } from '../types/GetDatasourcesByIDResponse'; @@ -104,9 +109,6 @@ export async function deleteAnomalyPolicy(authRequest: AuthRequest, restAPIReque * @returns {Promise} A promise that resolves when all policies have been deleted. */ export async function deletePolicies(restAPIRequest: RestAPIRequest, token: string): Promise { - if (process.env.CLEAN_UP !== 'true') { - return; - } const policyResponse = await restAPIRequest.getPolicies(token); const policyResponseBody = (await policyResponse.json()) as PolicyBudgetAndQuotaResponse; for (const policy of policyResponseBody.organization_constraints) { @@ -115,6 +117,26 @@ export async function deletePolicies(restAPIRequest: RestAPIRequest, token: stri } } +/** + * Deletes all tagging policies associated with the organization. + * + * This function retrieves a list of tagging policies using the provided `RestAPIRequest` instance + * and iterates through each policy in the `organization_constraints` array. For each policy, + * it logs the policy name and ID, then sends a request to delete the policy. + * + * @param {RestAPIRequest} restAPIRequest - An instance of the `RestAPIRequest` class used to interact with the REST API. + * @param {string} token - The authorization token used for API requests. + * @returns {Promise} A promise that resolves when all tagging policies have been deleted. + */ +export async function deleteTaggingPolicies(restAPIRequest: RestAPIRequest, token: string): Promise { + const taggingPolicyResponse = await restAPIRequest.getTaggingPolicies(token); + const taggingPolicyResponseBody = (await taggingPolicyResponse.json()) as TaggingPolicyResponse; + for (const policy of taggingPolicyResponseBody.organization_constraints) { + debugLog(`Deleting tagging policy: ${policy.name} with ID: ${policy.id}`); + await restAPIRequest.deletePolicy(policy.id, token); + } +} + /** * Retrieves the IDs of sub-pools whose names start with the specified pool name. *