From 5207fda30394c3a6cd3a91d314e0682aeb8aec6c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sat, 12 Jul 2025 13:58:39 +0300 Subject: [PATCH] [ResponseOps] Fixes embeddable alerts panel flaky test (#227390) ## Summary I changed the code to use API calls to create resources like rules and install the data sample data instead of using the UI. I also fixed a small issue with the time range selector that caused the tests to fail. Fixes: https://github.com/elastic/kibana/issues/220807. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios (cherry picked from commit 45db4add3c3365c9538adf35529577462e5baf6d) --- x-pack/test/functional/services/rules/api.ts | 10 + .../embeddable_alerts_table.ts | 284 +++++++++--------- 2 files changed, 157 insertions(+), 137 deletions(-) diff --git a/x-pack/test/functional/services/rules/api.ts b/x-pack/test/functional/services/rules/api.ts index 8ad6e45ee8572..ab088a30810a3 100644 --- a/x-pack/test/functional/services/rules/api.ts +++ b/x-pack/test/functional/services/rules/api.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { RuleResponse } from '@kbn/alerting-plugin/common/routes/rule/response'; import { FtrProviderContext } from '../../ftr_provider_context'; export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { @@ -20,6 +21,7 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { ruleTypeId, schedule, actions = [], + tags = [], }: { consumer: string; name: string; @@ -28,6 +30,7 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { ruleTypeId: string; schedule: Record; actions?: any[]; + tags?: string[]; }) { log.debug(`Create basic rule...`); const { body: createdRule } = await kbnSupertest @@ -41,6 +44,7 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { rule_type_id: ruleTypeId, schedule, actions, + tags, }) .expect(200); return createdRule; @@ -67,5 +71,11 @@ export function RulesAPIServiceProvider({ getService }: FtrProviderContext) { await this.deleteRule(rule.id); } }, + + async getRule(id: string): Promise { + log.debug(`Getting rule with id ${id}...`); + const { body: rule } = await kbnSupertest.get(`/api/alerting/rule/${id}`).expect(200); + return rule; + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts b/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts index d2887dffcd603..a2b868afe045e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts +++ b/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/embeddable_alerts_table.ts @@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'dashboard', 'timePicker', ]); - const browser = getService('browser'); + const security = getService('security'); const retry = getService('retry'); const find = getService('find'); @@ -42,153 +42,40 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const comboBox = getService('comboBox'); const dashboardAddPanel = getService('dashboardAddPanel'); const toasts = getService('toasts'); + const sampleData = getService('sampleData'); + const rules = getService('rules'); + const es = getService('es'); + const config = getService('config'); + const retryTimeout = config.get('timeouts.try'); - const createEsQueryRule = async (index: string, solution: 'stack' | 'observability') => { - const name = `${solution}-rule`; - const { body: createdRule } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send({ - name, - rule_type_id: `.es-query`, - enabled: true, - schedule: { interval: '5s' }, - consumer: solution === 'stack' ? 'stackAlerts' : 'logs', - tags: [name], - params: { - searchConfiguration: { - query: { - query: '', - language: 'kuery', - }, - index, - }, - timeField: 'timestamp', - searchType: 'searchSource', - timeWindowSize: 5, - timeWindowUnit: 'h', - threshold: [-1], - thresholdComparator: '>', - size: 1, - aggType: 'count', - groupBy: 'all', - termSize: 5, - excludeHitsFromPreviousRun: false, - sourceFields: [], - }, - }) - .expect(200); - objectRemover.add(createdRule.id, 'rule', 'alerting'); - return createdRule; - }; - - const createSecurityRule = async (index: string) => { - const { body: createdRule } = await supertest - .post(`/api/detection_engine/rules`) - .set('kbn-xsrf', 'foo') - .send({ - type: 'query', - filters: [], - language: 'kuery', - query: '_id: *', - required_fields: [], - data_view_id: index, - author: [], - false_positives: [], - references: [], - risk_score: 21, - risk_score_mapping: [], - severity: 'low', - severity_mapping: [], - threat: [], - max_signals: 100, - name: 'security-rule', - description: 'security-rule', - tags: ['security-rule'], - setup: '', - license: '', - interval: '5s', - from: 'now-10m', - to: 'now', - actions: [], - enabled: true, - meta: { - kibana_siem_app_url: 'http://localhost:5601/app/security', - }, - }) - .expect(200); - objectRemover.add(createdRule.id, 'rule', 'alerting'); - return createdRule; - }; - - const getSampleWebLogsDataView = async () => { - const { body } = await supertest - .post(`/api/content_management/rpc/search`) - .set('kbn-xsrf', 'foo') - .send({ - contentTypeId: 'index-pattern', - query: { limit: 10 }, - version: 1, - }) - .expect(200); - return body.result.result.hits.find( - (dataView: { attributes: { title: string } }) => - dataView.attributes.title === 'kibana_sample_data_logs' - ); - }; + describe('Embeddable alerts panel', () => { + before(async () => { + await sampleData.testResources.installAllKibanaSampleData(); - const getRuleSummary = async (ruleId: string) => { - const { body: summary } = await supertest - .get(`/internal/alerting/rule/${encodeURIComponent(ruleId)}/_alert_summary`) - .expect(200); - return summary; - }; + const dataViews = await getDataViews(); + const sampleDataLogsDataView = dataViews.find( + (dataView) => dataView.title === 'kibana_sample_data_logs' + )!; - // Failing: See https://github.com/elastic/kibana/issues/220807 - describe.skip('Embeddable alerts panel', () => { - before(async () => { - await pageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await pageObjects.home.addSampleDataSet('logs'); - const dataView = await getSampleWebLogsDataView(); const [stackRule, observabilityRule, securityRule] = await Promise.all([ - createEsQueryRule(dataView.id, 'stack'), - createEsQueryRule(dataView.id, 'observability'), - createSecurityRule(dataView.id), + createEsQueryRule(sampleDataLogsDataView.id, 'stack'), + createEsQueryRule(sampleDataLogsDataView.id, 'observability'), + createSecurityRule(sampleDataLogsDataView.id), ]); - // Refresh to see the created rules - await browser.refresh(); - await pageObjects.header.waitUntilLoadingHasFinished(); + await waitForRuleToBecomeActive(stackRule.id); + await waitForRuleToBecomeActive(observabilityRule.id); + await waitForRuleToBecomeActive(securityRule.id); - // Wait for all rules to have created alerts - const rulesAlerted: Record = { - [stackRule.id]: false, - [observabilityRule.id]: false, - [securityRule.id]: false, - }; - await retry.try(async () => { - const rulesWithoutAlerts = Object.entries(rulesAlerted) - .filter(([_, alerted]) => !alerted) - .map(([ruleId]) => ruleId); - await Promise.all( - rulesWithoutAlerts.map(async (ruleId) => { - const summary = await getRuleSummary(ruleId); - rulesAlerted[ruleId] = Object.keys(summary.alerts).length > 0; - }) - ); - expect(Object.values(rulesAlerted).every((hasAlerts) => hasAlerts)).to.be(true); - }); + await waitForAlertsToBeCreated(stackRule.id); + await waitForAlertsToBeCreated(observabilityRule.id); + await waitForAlertsToBeCreated(securityRule.id); await pageObjects.dashboard.gotoDashboardURL(); }); after(async () => { - await pageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await pageObjects.home.addSampleDataSet('logs'); + await sampleData.testResources.removeAllKibanaSampleData(); await objectRemover.removeAll(); }); @@ -336,11 +223,134 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector( '[data-test-subj=customizePanel] [data-test-subj=superDatePickerToggleQuickMenuButton]' ); - await testSubjects.click('superDatePickerCommonlyUsed_Last_24 hours'); + await testSubjects.click('superDatePickerCommonlyUsed_sample_data range'); await testSubjects.click('saveCustomizePanelButton'); await retry.try(async () => expect((await testSubjects.findAll('alertsTableEmptyState')).length).to.equal(1) ); }); }); + + const createEsQueryRule = async (index: string, solution: 'stack' | 'observability') => { + const name = `${solution}-rule`; + const createdRule = await rules.api.createRule({ + name, + ruleTypeId: `.es-query`, + schedule: { interval: '5s' }, + consumer: solution === 'stack' ? 'stackAlerts' : 'logs', + tags: [name], + params: { + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index, + }, + timeField: 'timestamp', + searchType: 'searchSource', + timeWindowSize: 5, + timeWindowUnit: 'h', + threshold: [-1], + thresholdComparator: '>', + size: 1, + aggType: 'count', + groupBy: 'all', + termSize: 5, + excludeHitsFromPreviousRun: false, + sourceFields: [], + }, + }); + + objectRemover.add(createdRule.id, 'rule', 'alerting'); + + return createdRule; + }; + + const createSecurityRule = async (index: string) => { + const { body: createdRule } = await supertest + .post(`/api/detection_engine/rules`) + .set('kbn-xsrf', 'foo') + .send({ + type: 'query', + filters: [], + language: 'kuery', + query: '_id: *', + required_fields: [], + data_view_id: index, + author: [], + false_positives: [], + references: [], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + threat: [], + max_signals: 100, + name: 'security-rule', + description: 'security-rule', + tags: ['security-rule'], + setup: '', + license: '', + interval: '5s', + from: 'now-10m', + to: 'now', + actions: [], + enabled: true, + meta: { + kibana_siem_app_url: 'http://localhost:5601/app/security', + }, + }) + .expect(200); + + objectRemover.add(createdRule.id, 'rule', 'alerting'); + + return createdRule; + }; + + const waitForAlertsToBeCreated = async (ruleId: string) => { + return await retry.tryForTime(retryTimeout, async () => { + const response = await es.search({ + index: '.alerts*', + query: { + bool: { + filter: [ + { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + ], + }, + }, + }); + + if (response.hits.hits.length === 0) { + throw new Error(`No hits found for index .alerts* and ruleId ${ruleId}`); + } + + return response; + }); + }; + + const waitForRuleToBecomeActive = async (ruleId: string) => { + return await retry.tryForTime(retryTimeout, async () => { + const rule = await rules.api.getRule(ruleId); + + const { execution_status: executionStatus } = rule || {}; + const { status } = executionStatus || {}; + + if (status === 'active' || status === 'ok') { + return executionStatus?.status; + } + + throw new Error(`waitForStatus(active|ok): got ${status}`); + }); + }; + + const getDataViews = async (): Promise> => { + const response = await supertest.get('/api/data_views').expect(200); + + return response.body.data_view; + }; };