test(e2e): refactor pipeline condition tests and fix data ingestion timeout (#9528)

## Summary
Refactored pipeline condition tests into 2 new spec files for better
organization and maintainability, fixed data ingestion timeout issue,
and added comprehensive test coverage for pipeline conditions.

## Changes
- Refactored pipeline condition tests into 2 new spec files for better
organization
- Added 14 new comprehensive test cases (7 UI-based + 7 API-based
validation tests)
- Fixed beforeEach timeout issue by reducing data ingestion from 3,800+
to 10 records per stream
- Added cleanup patterns for `e2e_conditions_*` streams to prevent data
accumulation
- Updated apiCleanup.js to include 7 new stream names in pipeline
cleanup
- Removed outdated comments referencing conditional wait logic
- Added page object methods for condition testing in pipelinesPage.js,
logsPage.js, and streamsPage.js

## Test Coverage

### pipeline-conditions.spec.js (7 tests - all passing ✅)
- Basic condition operations with multiple operators (=, !=, Contains,
NotContains, >=)
- Condition groups, nesting, and reordering
- Field validation and error messages
- Complex operator precedence (A OR B AND C)
- Multiple conditions (A, B, C, D)
- Condition node deletion
- Comparison operators validation

### pipeline-conditions-validation.spec.js (7 tests - API-based)
- Operator precedence validation
- Nested groups with AND/OR logic
- Numeric comparisons
- String operators (case-insensitive Contains)

## Performance
- Reduced test execution time from 180s timeout to ~4.6 minutes for full
suite
- 100% pass rate for active UI tests (7/7)

## Files Changed
- `.github/workflows/playwright.yml` - Updated test configuration
- `tests/test-data/data_100.json` - Added test data for validation tests
- `tests/test-data/sample_pipeline_template.json` - Added pipeline
template
- `tests/ui-testing/pages/apiCleanup.js` - Added stream cleanup patterns
- `tests/ui-testing/pages/pipelinesPages/pipelinesPage.js` - Added
condition testing methods
- `tests/ui-testing/pages/logsPages/logsPage.js` - Added data ingestion
helpers
- `tests/ui-testing/pages/streamsPages/streamsPage.js` - Added stream
management helpers
-
`tests/ui-testing/playwright-tests/Pipelines/pipeline-conditions.spec.js`
- Refactored UI tests
-
`tests/ui-testing/playwright-tests/Pipelines/pipeline-conditions-validation.spec.js`
- New API validation tests
- `tests/ui-testing/playwright-tests/cleanup.spec.js` - Added
e2e_conditions_* pattern

## Testing
All 7 UI-based tests passing with 100% pass rate.
This commit is contained in:
Shrinath Rao
2025-12-09 15:49:28 +05:30
committed by GitHub
parent 085e065b8d
commit 6ae38d5ce2
10 changed files with 2501 additions and 70 deletions
+1
View File
@@ -194,6 +194,7 @@ jobs:
browser: "chrome"
run_files: [
"pipeline-conditions.spec.js",
"pipeline-conditions-validation.spec.js",
"pipelines.spec.js",
"pipeline-dynamic.spec.js",
"pipeline-core.spec.js",
File diff suppressed because one or more lines are too long
@@ -0,0 +1,180 @@
{
"pipeline_id": "7402696020770947072",
"version": 0,
"enabled": true,
"org": "default",
"name": "test",
"description": "",
"source": {
"source_type": "realtime",
"org_id": "default",
"stream_name": "default",
"stream_type": "logs"
},
"nodes": [
{
"id": "9aab959a-0d22-42a4-be84-f15488862bfa",
"data": {
"node_type": "stream",
"org_id": "default",
"stream_name": "default",
"stream_type": "logs"
},
"position": {
"x": 597,
"y": 222.5
},
"io_type": "input"
},
{
"id": "95c8ef15-2b44-4ba7-87c8-efc28b0d7b59",
"data": {
"node_type": "stream",
"org_id": "default",
"stream_name": "default",
"stream_type": "logs"
},
"position": {
"x": 602.5,
"y": 537.5
},
"io_type": "output"
},
{
"id": "c160107d-ba19-427d-8e9b-95cca5973373",
"data": {
"node_type": "condition",
"version": 2,
"conditions": {
"filterType": "group",
"logicalOperator": "AND",
"conditions": [
{
"filterType": "condition",
"column": "code",
"operator": "=",
"value": "test",
"logicalOperator": "AND"
},
{
"filterType": "condition",
"column": "code",
"operator": ">=",
"value": "test",
"logicalOperator": "OR"
},
{
"filterType": "condition",
"column": "code",
"operator": "contains",
"value": "test",
"logicalOperator": "AND"
},
{
"filterType": "condition",
"column": "",
"operator": "=",
"value": "",
"logicalOperator": "AND"
},
{
"filterType": "condition",
"column": "field",
"operator": "=",
"value": "rrewt",
"logicalOperator": "AND"
},
{
"filterType": "group",
"logicalOperator": "OR",
"conditions": [
{
"filterType": "condition",
"column": "code",
"operator": "<=",
"value": "test",
"logicalOperator": "OR"
},
{
"filterType": "condition",
"column": "code",
"operator": ">",
"value": "test",
"logicalOperator": "OR"
},
{
"filterType": "condition",
"column": "code",
"operator": "<",
"value": "test",
"logicalOperator": "AND"
}
]
},
{
"filterType": "group",
"logicalOperator": "AND",
"conditions": [
{
"filterType": "condition",
"column": "code",
"operator": "not_contains",
"value": "test",
"logicalOperator": "OR"
}
]
}
]
}
},
"position": {
"x": 206.75,
"y": 392.5
},
"io_type": "default"
}
],
"edges": [
{
"id": "e9aab959a-0d22-42a4-be84-f15488862bfa-c160107d-ba19-427d-8e9b-95cca5973373",
"source": "9aab959a-0d22-42a4-be84-f15488862bfa",
"target": "c160107d-ba19-427d-8e9b-95cca5973373",
"markerEnd": {
"type": "arrowclosed",
"width": 20,
"height": 20
},
"type": "custom",
"style": {
"strokeWidth": 2
},
"animated": true,
"updatable": true
},
{
"id": "ec160107d-ba19-427d-8e9b-95cca5973373-95c8ef15-2b44-4ba7-87c8-efc28b0d7b59",
"source": "c160107d-ba19-427d-8e9b-95cca5973373",
"target": "95c8ef15-2b44-4ba7-87c8-efc28b0d7b59",
"markerEnd": {
"type": "arrowclosed",
"width": 20,
"height": 20
},
"type": "custom",
"style": {
"strokeWidth": 2
},
"animated": true,
"updatable": true
}
],
"paused_at": null,
"type": "realtime",
"stream_name": "default",
"stream_type": "logs",
"frequency": "--",
"period": "--",
"cron": "--",
"sql_query": "--",
"#": 1
}
+422 -5
View File
@@ -573,7 +573,25 @@ class APICleanup {
* Deletes pipelines where source.stream_name matches any of the target streams
* @param {Array<string>} streamNames - Array of stream names to match (default: ['e2e_automate', 'e2e_automate1', 'e2e_automate2', 'e2e_automate3'])
*/
async cleanupPipelines(streamNames = ['e2e_automate', 'e2e_automate1', 'e2e_automate2', 'e2e_automate3']) {
async cleanupPipelines(streamNames = [
'e2e_automate',
'e2e_automate1',
'e2e_automate2',
'e2e_automate3',
'e2e_conditions_validation_precedence_src',
'e2e_conditions_validation_nested_and_src',
'e2e_conditions_validation_nested_or_src',
'e2e_conditions_validation_numeric_src',
'e2e_conditions_validation_contains_src',
'e2e_conditions_validation_deep_src',
'e2e_conditions_basic',
'e2e_conditions_groups',
'e2e_conditions_validation',
'e2e_conditions_precedence',
'e2e_conditions_multiple',
'e2e_conditions_delete',
'e2e_conditions_operators'
]) {
testLogger.info('Starting pipeline cleanup', { streams: streamNames });
try {
@@ -581,10 +599,61 @@ class APICleanup {
const pipelines = await this.fetchPipelines();
testLogger.info('Fetched pipelines', { total: pipelines.length });
// Filter pipelines by source stream name
const matchingPipelines = pipelines.filter(p =>
p.source && streamNames.includes(p.source.stream_name)
);
// Filter pipelines by source stream name OR by pattern matching
const matchingPipelines = pipelines.filter(p => {
if (!p.source) return false;
// Exact match for stream names
if (streamNames.includes(p.source.stream_name)) return true;
// Pattern match for validation tests (old naming - static names)
if (p.source.stream_name && p.source.stream_name.startsWith('e2e_conditions_validation_')) return true;
// Pattern match for timestamp-based test pipelines (new naming)
const sourceStreamPatterns = [
/^e2e_precedence_src_\d+$/,
/^e2e_multiple_or_src_\d+$/,
/^e2e_nested_or_src_\d+$/,
/^e2e_numeric_src_\d+$/,
/^e2e_multiple_and_src_\d+$/,
/^e2e_deep_src_\d+$/,
/^e2e_not_operator_src_\d+$/,
/^e2e_impossible_src_\d+$/,
/^e2e_universal_src_\d+$/,
/^e2e_4level_src_\d+$/,
/^simple_src_\d+$/,
/^manual_debug_src_/,
/^manual_verify_src_/
];
if (p.source.stream_name && sourceStreamPatterns.some(pattern => pattern.test(p.source.stream_name))) {
return true;
}
// Also match by pipeline name patterns for comprehensive cleanup
const pipelineNamePatterns = [
/^validation-precedence-\d+$/,
/^validation-multiple-or-\d+$/,
/^validation-nested-or-\d+$/,
/^validation-numeric-\d+$/,
/^validation-multiple-and-\d+$/,
/^validation-deep-nested-\d+$/,
/^validation-not-operator-\d+$/,
/^validation-impossible-\d+$/,
/^validation-universal-\d+$/,
/^validation-4level-\d+$/,
/^simple-test-\d+$/,
/^or-root-test-\d+$/,
/^manual-debug-pipeline-/,
/^manual-verify-pipeline-/
];
if (p.name && pipelineNamePatterns.some(pattern => pattern.test(p.name))) {
return true;
}
return false;
});
testLogger.info('Found pipelines matching target streams', { count: matchingPipelines.length });
if (matchingPipelines.length === 0) {
@@ -1827,6 +1896,354 @@ class APICleanup {
testLogger.error('Complete cascade cleanup failed', { error: error.message });
}
}
/**
* ==========================================
* PIPELINE-SPECIFIC API METHODS
* ==========================================
*/
/**
* Clean conditions for API (removes UI-only fields)
* This removes fields like 'id' and 'groupId' that are only used in UI
* @param {object} conditions - Condition configuration object with UI fields
* @returns {object} Cleaned conditions for API
*/
cleanConditionsForAPI(conditions) {
if (conditions.filterType === "condition") {
// Remove UI-only fields: id, groupId
// KEEP values field as it's required by the backend
const { id, groupId, ...cleaned } = conditions;
return cleaned;
} else if (conditions.filterType === "group") {
// Recursively clean nested conditions
const { id, groupId, ...cleaned } = conditions;
cleaned.conditions = conditions.conditions.map(c => this.cleanConditionsForAPI(c));
return cleaned;
}
return conditions;
}
/**
* Create a stream via API
* @param {string} streamName - Name of the stream to create
* @param {string} streamType - Type of stream (logs, metrics, traces)
* @returns {Promise<object>} API response
*/
async createStream(streamName, streamType = 'logs') {
const payload = {
fields: [],
settings: {
partition_keys: [],
index_fields: [],
full_text_search_keys: [],
bloom_filter_fields: [],
defined_schema_fields: [],
data_retention: 14
}
};
testLogger.info('Creating stream via API', { streamName, streamType });
try {
const response = await fetch(`${this.baseUrl}/api/${this.org}/streams/${streamName}?type=${streamType}`, {
method: 'POST',
headers: {
'Authorization': this.authHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.status === 200 && data.code === 200) {
testLogger.info('Stream created successfully', { streamName });
} else {
testLogger.warn('Stream creation returned non-200 or already exists', { streamName, status: response.status, data });
}
return { status: response.status, data };
} catch (error) {
testLogger.error('Failed to create stream', { streamName, error: error.message });
return { status: 500, error: error.message };
}
}
/**
* Create a pipeline via API
* @param {string} pipelineName - Name of the pipeline
* @param {string} sourceStream - Source stream name
* @param {string} destStream - Destination stream name
* @param {object} conditions - Condition configuration object
* @returns {Promise<object>} API response
*/
async createPipeline(pipelineName, sourceStream, destStream, conditions) {
// Clean conditions to remove UI-only fields
const cleanedConditions = this.cleanConditionsForAPI(conditions);
testLogger.info('Cleaned conditions', { cleaned: JSON.stringify(cleanedConditions, null, 2) });
// Generate unique node IDs
const inputNodeId = `input-${Date.now()}`;
const conditionNodeId = `condition-${Date.now()}`;
const outputNodeId = `output-${Date.now()}`;
const pipelinePayload = {
pipeline_id: "",
version: 0,
enabled: true,
org: this.org,
name: pipelineName,
description: `Validation test pipeline for ${pipelineName}`,
source: {
source_type: "realtime"
},
paused_at: null,
nodes: [
{
id: inputNodeId,
position: { x: 100, y: 100 },
data: {
node_type: "stream",
stream_type: "logs",
stream_name: sourceStream,
org_id: this.org
},
io_type: "input"
},
{
id: conditionNodeId,
position: { x: 300, y: 200 },
data: {
node_type: "condition",
version: 2,
conditions: cleanedConditions
},
io_type: "default"
},
{
id: outputNodeId,
position: { x: 500, y: 300 },
data: {
node_type: "stream",
stream_type: "logs",
stream_name: destStream,
org_id: this.org
},
io_type: "output"
}
],
edges: [
{
id: `e-${inputNodeId}-${conditionNodeId}`,
source: inputNodeId,
target: conditionNodeId,
markerEnd: {
type: "arrowclosed",
width: 20,
height: 20
},
type: "custom",
style: {
strokeWidth: 2
},
animated: true,
updatable: true
},
{
id: `e-${conditionNodeId}-${outputNodeId}`,
source: conditionNodeId,
target: outputNodeId,
markerEnd: {
type: "arrowclosed",
width: 20,
height: 20
},
type: "custom",
style: {
strokeWidth: 2
},
animated: true,
updatable: true
}
],
type: "realtime",
stream_name: sourceStream,
stream_type: "logs"
};
testLogger.info('Creating pipeline via API', { pipelineName, sourceStream, destStream });
testLogger.info('Pipeline payload', { payload: JSON.stringify(pipelinePayload, null, 2) });
try {
const response = await fetch(`${this.baseUrl}/api/${this.org}/pipelines`, {
method: 'POST',
headers: {
'Authorization': this.authHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify(pipelinePayload)
});
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
const text = await response.text();
data = { error: text };
}
testLogger.info('Pipeline creation response', { pipelineName, status: response.status, data });
if (response.status !== 200) {
throw new Error(`Pipeline creation failed: ${JSON.stringify(data)}`);
}
return { status: response.status, data };
} catch (error) {
testLogger.error('Failed to create pipeline', { pipelineName, error: error.message });
throw error;
}
}
/**
* Ingest data to a stream
* Sends records individually to ensure uniqueness
* @param {string} streamName - Stream name
* @param {array} data - Array of log objects
* @returns {Promise<object>} Result with success/fail counts
*/
async ingestData(streamName, data) {
testLogger.info('Ingesting data', { streamName, recordCount: data.length });
let successCount = 0;
let failCount = 0;
// Send records one by one to ensure each is treated as unique
for (let i = 0; i < data.length; i++) {
const record = data[i];
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${this.org}/${streamName}/_json`, {
method: 'POST',
headers: {
'Authorization': this.authHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify([record]) // Send as single-element array
});
const responseData = await response.json();
if (response.status === 200 && responseData.code === 200) {
successCount++;
testLogger.debug(`Ingested record ${i+1}/${data.length}`, { name: record.name, test_id: record.test_id || 'no_id', unique_id: record.unique_id });
} else {
failCount++;
testLogger.error(`Failed to ingest record ${i+1}/${data.length}`, { response: responseData, test_id: record.test_id || record.name });
}
// Small delay between records
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
failCount++;
testLogger.error(`Error ingesting record ${i+1}/${data.length}`, { error: error.message });
}
}
testLogger.info('Ingestion complete', { streamName, total: data.length, success: successCount, failed: failCount });
if (failCount > 0) {
throw new Error(`Failed to ingest ${failCount} out of ${data.length} records`);
}
return { total: data.length, success: successCount, failed: failCount };
}
/**
* Query stream via API
* @param {string} streamName - Stream to query
* @param {number} expectedMinCount - Minimum expected record count (optional)
* @returns {Promise<array>} Query results
*/
async queryStream(streamName, expectedMinCount = null) {
testLogger.info('Querying stream via API', { streamName });
// Query for last 10 minutes
const endTime = Date.now() * 1000; // microseconds
const startTime = endTime - (10 * 60 * 1000 * 1000);
const query = {
query: {
sql: `SELECT * FROM "${streamName}"`,
start_time: startTime,
end_time: endTime,
from: 0,
size: 1000
}
};
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${this.org}/_search?type=logs`, {
method: 'POST',
headers: {
'Authorization': this.authHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify(query)
});
const data = await response.json();
const results = data.hits || [];
testLogger.info('Query results', { streamName, recordCount: results.length });
if (expectedMinCount !== null && expectedMinCount !== 0) {
if (results.length < expectedMinCount) {
throw new Error(`Expected at least ${expectedMinCount} records, got ${results.length}`);
}
}
return results;
} catch (error) {
testLogger.error('Failed to query stream', { streamName, error: error.message });
throw error;
}
}
/**
* Verify stream exists via API
* @param {string} streamName - Name of the stream to verify
* @returns {Promise<boolean>} True if stream exists
*/
async verifyStreamExists(streamName) {
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${this.org}/streams`, {
method: 'GET',
headers: {
'Authorization': this.authHeader,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.status === 200 && data.list) {
const streamExists = data.list.some(s => s.name === streamName);
testLogger.info('Stream existence check', { streamName, exists: streamExists });
return streamExists;
}
testLogger.warn('Failed to check stream existence', { streamName, status: response.status });
return false;
} catch (error) {
testLogger.error('Error checking stream existence', { streamName, error: error.message });
return false;
}
}
}
module.exports = APICleanup;
+68 -1
View File
@@ -2820,4 +2820,71 @@ export class LogsPage {
// Click outside to trigger validation
return await this.page.locator('body').click({ position: { x: 0, y: 0 } });
}
}
/**
* ==========================================
* DATA INGESTION API METHOD
* ==========================================
*/
/**
* Ingest data to a stream
* Sends records individually to ensure uniqueness
* @param {string} streamName - Stream name
* @param {array} data - Array of log objects
* @returns {Promise<object>} Result with success/fail counts
*/
async ingestData(streamName, data) {
const fetch = (await import('node-fetch')).default;
const orgId = process.env["ORGNAME"];
const basicAuthCredentials = Buffer.from(
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
).toString('base64');
testLogger.info('Ingesting data', { streamName, recordCount: data.length });
let successCount = 0;
let failCount = 0;
// Send records one by one to ensure each is treated as unique
for (let i = 0; i < data.length; i++) {
const record = data[i];
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${orgId}/${streamName}/_json`, {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuthCredentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify([record]) // Send as single-element array
});
const responseData = await response.json();
if (response.status === 200 && responseData.code === 200) {
successCount++;
testLogger.debug(`Ingested record ${i+1}/${data.length}`, { name: record.name, test_id: record.test_id || 'no_id', unique_id: record.unique_id });
} else {
failCount++;
testLogger.error(`Failed to ingest record ${i+1}/${data.length}`, { response: responseData, test_id: record.test_id || record.name });
}
// Small delay between records
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
failCount++;
testLogger.error(`Error ingesting record ${i+1}/${data.length}`, { error: error.message });
}
}
testLogger.info('Ingestion complete', { streamName, total: data.length, success: successCount, failed: failCount });
if (failCount > 0) {
throw new Error(`Failed to ingest ${failCount} out of ${data.length} records`);
}
return { total: data.length, success: successCount, failed: failCount };
}
}
@@ -1,6 +1,7 @@
// pipelinesPage.js
const { expect } = require('@playwright/test')
const testLogger = require('../../playwright-tests/utils/test-logger.js');
const fetch = require('node-fetch');
const randomNodeName = `remote-node-${Math.floor(Math.random() * 1000)}`;
@@ -934,4 +935,144 @@ export class PipelinesPage {
await this.confirmButton.click();
}
}
/**
* ==========================================
* PIPELINE API METHODS
* ==========================================
*/
/**
* Clean conditions for API (removes UI-only fields)
* This removes fields like 'id' and 'groupId' that are only used in UI
* @param {object} conditions - Condition configuration object with UI fields
* @returns {object} Cleaned conditions for API
*/
cleanConditionsForAPI(conditions) {
if (conditions.filterType === "condition") {
// Remove UI-only fields: id, groupId
// KEEP values field as it's required by the backend
const { id, groupId, ...cleaned } = conditions;
return cleaned;
} else if (conditions.filterType === "group") {
// Recursively clean nested conditions
const { id, groupId, ...cleaned } = conditions;
cleaned.conditions = conditions.conditions.map(c => this.cleanConditionsForAPI(c));
return cleaned;
}
return conditions;
}
/**
* Create a pipeline via API
* @param {string} pipelineName - Name of the pipeline
* @param {string} sourceStream - Source stream name
* @param {string} destStream - Destination stream name
* @param {object} conditions - Condition configuration object
* @returns {Promise<object>} API response
*/
async createPipeline(pipelineName, sourceStream, destStream, conditions) {
const orgId = process.env["ORGNAME"];
const basicAuthCredentials = Buffer.from(
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
).toString('base64');
const headers = {
"Authorization": `Basic ${basicAuthCredentials}`,
"Content-Type": "application/json",
};
// Clean conditions to remove UI-only fields
const cleanedConditions = this.cleanConditionsForAPI(conditions);
testLogger.info('Cleaned conditions', { cleaned: JSON.stringify(cleanedConditions, null, 2) });
// Generate unique node IDs
const inputNodeId = `input-${Date.now()}`;
const conditionNodeId = `condition-${Date.now()}`;
const outputNodeId = `output-${Date.now()}`;
const pipelinePayload = {
pipeline_id: "",
version: 0,
enabled: true, // Enable pipeline for realtime processing
org: orgId,
name: pipelineName,
description: `Validation test pipeline for ${pipelineName}`,
source: {
source_type: "realtime"
},
paused_at: null,
nodes: [
{
id: inputNodeId,
position: { x: 100, y: 100 },
data: {
node_type: "stream",
stream_type: "logs",
stream_name: sourceStream,
org_id: orgId
},
io_type: "input"
},
{
id: conditionNodeId,
position: { x: 300, y: 200 },
data: {
node_type: "condition",
version: 2,
conditions: cleanedConditions
},
io_type: "intermediate"
},
{
id: outputNodeId,
position: { x: 500, y: 100 },
data: {
node_type: "stream",
stream_type: "logs",
stream_name: destStream,
org_id: orgId
},
io_type: "output"
}
],
edges: [
{
id: `${inputNodeId}-${conditionNodeId}`,
source: inputNodeId,
target: conditionNodeId,
sourceHandle: "success"
},
{
id: `${conditionNodeId}-${outputNodeId}`,
source: conditionNodeId,
target: outputNodeId,
sourceHandle: "success"
}
]
};
testLogger.info('Creating pipeline via API', { pipelineName, sourceStream, destStream });
try {
const response = await fetch(`${process.env.ZO_BASE_URL}/api/${orgId}/pipelines`, {
method: 'POST',
headers: headers,
body: JSON.stringify(pipelinePayload)
});
const data = await response.json();
if (response.status === 200 && data.code === 200) {
testLogger.info('Pipeline created successfully', { pipelineName });
} else {
testLogger.warn('Pipeline creation returned non-200', { pipelineName, status: response.status, data });
}
return { status: response.status, data };
} catch (error) {
testLogger.error('Failed to create pipeline', { pipelineName, error: error.message });
return { status: 500, error: error.message };
}
}
}
@@ -5,6 +5,7 @@ import { IngestionPage } from '../generalPages/ingestionPage.js';
import { ManagementPage } from '../generalPages/managementPage.js';
import { getHeaders, getIngestionUrl, sendRequest } from '../../utils/apiUtils.js';
const testLogger = require('../../playwright-tests/utils/test-logger.js');
export class StreamsPage {
constructor(page) {
@@ -453,4 +454,141 @@ export class StreamsPage {
async waitForUI(milliseconds) {
await this.page.waitForTimeout(milliseconds);
}
}
/**
* ==========================================
* STREAM API METHODS
* ==========================================
*/
/**
* Create a stream via API
* @param {string} streamName - Name of the stream to create
* @param {string} streamType - Type of stream (logs, metrics, traces)
* @returns {Promise<object>} API response
*/
async createStream(streamName, streamType = 'logs') {
const fetch = (await import('node-fetch')).default;
const orgId = process.env["ORGNAME"];
const headers = getHeaders();
const payload = {
fields: [],
settings: {
partition_keys: [],
index_fields: [],
full_text_search_keys: [],
bloom_filter_fields: [],
defined_schema_fields: [],
data_retention: 14
}
};
testLogger.info('Creating stream via API', { streamName, streamType });
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${orgId}/streams/${streamName}?type=${streamType}`, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.status === 200 && data.code === 200) {
testLogger.info('Stream created successfully', { streamName });
} else {
testLogger.warn('Stream creation returned non-200 or already exists', { streamName, status: response.status });
}
return { status: response.status, data };
} catch (error) {
testLogger.error('Failed to create stream', { streamName, error: error.message });
return { status: 500, error: error.message };
}
}
/**
* Verify stream exists via API
* @param {string} streamName - Name of the stream to verify
* @returns {Promise<boolean>} True if stream exists
*/
async verifyStreamExists(streamName) {
const fetch = (await import('node-fetch')).default;
const orgId = process.env["ORGNAME"];
const headers = getHeaders();
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${orgId}/streams`, {
method: 'GET',
headers: headers
});
const data = await response.json();
if (response.status === 200 && data.list) {
const streamExists = data.list.some(s => s.name === streamName);
testLogger.info('Stream existence check', { streamName, exists: streamExists });
return streamExists;
}
testLogger.warn('Failed to check stream existence', { streamName, status: response.status });
return false;
} catch (error) {
testLogger.error('Error checking stream existence', { streamName, error: error.message });
return false;
}
}
/**
* Query stream via API
* @param {string} streamName - Stream to query
* @param {number} expectedMinCount - Minimum expected record count (optional)
* @returns {Promise<array>} Query results
*/
async queryStream(streamName, expectedMinCount = null) {
const fetch = (await import('node-fetch')).default;
const orgId = process.env["ORGNAME"];
const headers = getHeaders();
testLogger.info('Querying stream via API', { streamName });
// Query for last 10 minutes
const endTime = Date.now() * 1000; // microseconds
const startTime = endTime - (10 * 60 * 1000 * 1000);
const query = {
query: {
sql: `SELECT * FROM "${streamName}"`,
start_time: startTime,
end_time: endTime,
from: 0,
size: 1000
}
};
try {
const response = await fetch(`${process.env.INGESTION_URL}/api/${orgId}/_search?type=logs`, {
method: 'POST',
headers: headers,
body: JSON.stringify(query)
});
const data = await response.json();
const results = data.hits || [];
testLogger.info('Query results', { streamName, recordCount: results.length });
if (expectedMinCount !== null && expectedMinCount !== 0) {
if (results.length < expectedMinCount) {
throw new Error(`Expected at least ${expectedMinCount} records, got ${results.length}`);
}
}
return results;
} catch (error) {
testLogger.error('Failed to query stream', { streamName, error: error.message });
throw error;
}
}
}
@@ -1,8 +1,8 @@
import { test, expect } from "../baseFixtures.js";
import logData from "../../fixtures/log.json";
import logsdata from "../../../test-data/logs_data.json";
import PageManager from "../../pages/page-manager.js";
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const PageManager = require('../../pages/page-manager.js');
const logData = require("../../fixtures/log.json");
const logsdata = require("../../../test-data/logs_data.json");
test.describe.configure({ mode: "parallel" });
@@ -12,66 +12,41 @@ test.use({
}
});
async function login(page) {
await page.goto(process.env["ZO_BASE_URL"]);
if (await page.getByText('Login as internal user').isVisible()) {
await page.getByText('Login as internal user').click();
}
await page.waitForTimeout(1000);
await page
.locator('[data-cy="login-user-id"]')
.fill(process.env["ZO_ROOT_USER_EMAIL"]);
await page.locator("label").filter({ hasText: "Password *" }).click();
await page
.locator('[data-cy="login-password"]')
.fill(process.env["ZO_ROOT_USER_PASSWORD"]);
await page.locator('[data-cy="login-sign-in"]').click();
}
async function ingestion(page, streamName = "e2e_automate_conditions") {
const orgId = process.env["ORGNAME"];
const basicAuthCredentials = Buffer.from(
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
).toString('base64');
const headers = {
"Authorization": `Basic ${basicAuthCredentials}`,
"Content-Type": "application/json",
};
const response = await page.evaluate(async ({ url, headers, orgId, streamName, logsdata }) => {
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
method: 'POST',
headers: headers,
body: JSON.stringify(logsdata)
});
return await fetchResponse.json();
}, {
url: process.env.INGESTION_URL,
headers: headers,
orgId: orgId,
streamName: streamName,
logsdata: logsdata
});
testLogger.debug('API response received', { response, streamName });
}
test.describe("Pipeline Conditions - Comprehensive Tests", () => {
let pageManager;
test.beforeEach(async ({ page }) => {
await login(page);
// Variables for lightweight cleanup
let currentPipelineName;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
// Navigate to base URL with authentication
await navigateToBase(page);
pageManager = new PageManager(page);
await page.waitForTimeout(5000);
// Post-authentication stabilization wait
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// Ingest data to unique streams for each test to avoid conflicts
await ingestion(page, "e2e_conditions_basic");
await ingestion(page, "e2e_conditions_groups");
await ingestion(page, "e2e_conditions_validation");
await ingestion(page, "e2e_conditions_precedence");
await ingestion(page, "e2e_conditions_multiple");
await ingestion(page, "e2e_conditions_delete");
await ingestion(page, "e2e_conditions_operators");
// Use only first 10 records to avoid timeout (full dataset has 3800+ records)
const streamNames = [
"e2e_conditions_basic",
"e2e_conditions_groups",
"e2e_conditions_validation",
"e2e_conditions_precedence",
"e2e_conditions_multiple",
"e2e_conditions_delete",
"e2e_conditions_operators"
];
for (const streamName of streamNames) {
await pageManager.logsPage.ingestData(streamName, logsdata.slice(0, 10));
}
// Brief wait for stream schemas to be established
await page.waitForTimeout(3000);
await page.goto(
`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`
@@ -79,8 +54,24 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
await page.waitForTimeout(2000);
});
test.afterEach(async ({ page }) => {
// Lightweight pipeline cleanup
try {
if (currentPipelineName) {
await pageManager.apiCleanup.deletePipeline(currentPipelineName).catch(() => {});
testLogger.info('Cleaned up pipeline', { currentPipelineName });
}
} catch (error) {
testLogger.warn('Cleanup failed', { error: error.message });
}
// Reset variables
currentPipelineName = null;
});
test("should create, edit, and test basic condition operations with multiple operators", async ({ page }) => {
const pipelineName = `pipeline-basic-ops-${Math.random().toString(36).substring(7)}`;
currentPipelineName = pipelineName;
// Create pipeline with condition
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_basic");
@@ -121,7 +112,20 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
// Save and complete pipeline
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
await ingestion(page);
// Ingest test data to streams
const streamNames = [
"e2e_conditions_basic",
"e2e_conditions_groups",
"e2e_conditions_validation",
"e2e_conditions_precedence",
"e2e_conditions_multiple",
"e2e_conditions_delete",
"e2e_conditions_operators"
];
for (const streamName of streamNames) {
await pageManager.logsPage.ingestData(streamName, logsdata.slice(0, 10));
}
// Test 9: Edit existing condition
await pageManager.pipelinesPage.openPipelineMenu();
@@ -248,6 +252,7 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
test("should test complex conditions and operator precedence (A OR B AND C)", async ({ page }) => {
const pipelineName = `pipeline-complex-${Math.random().toString(36).substring(7)}`;
currentPipelineName = pipelineName;
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_precedence");
@@ -275,7 +280,20 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
}
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
await ingestion(page);
// Ingest test data to streams
const streamNames = [
"e2e_conditions_basic",
"e2e_conditions_groups",
"e2e_conditions_validation",
"e2e_conditions_precedence",
"e2e_conditions_multiple",
"e2e_conditions_delete",
"e2e_conditions_operators"
];
for (const streamName of streamNames) {
await pageManager.logsPage.ingestData(streamName, logsdata.slice(0, 10));
}
await pageManager.pipelinesPage.openPipelineMenu();
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
@@ -283,6 +301,7 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
test("should test multiple conditions: A, B, C, and D", async ({ page }) => {
const pipelineName = `pipeline-multiple-${Math.random().toString(36).substring(7)}`;
currentPipelineName = pipelineName;
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_multiple");
@@ -312,7 +331,20 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
await pageManager.pipelinesPage.verifyConditionCount(4);
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
await ingestion(page);
// Ingest test data to streams
const streamNames = [
"e2e_conditions_basic",
"e2e_conditions_groups",
"e2e_conditions_validation",
"e2e_conditions_precedence",
"e2e_conditions_multiple",
"e2e_conditions_delete",
"e2e_conditions_operators"
];
for (const streamName of streamNames) {
await pageManager.logsPage.ingestData(streamName, logsdata.slice(0, 10));
}
await pageManager.pipelinesPage.openPipelineMenu();
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
@@ -320,6 +352,7 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
test("should delete condition node from pipeline", async ({ page }) => {
const pipelineName = `pipeline-delete-${Math.random().toString(36).substring(7)}`;
currentPipelineName = pipelineName;
// Create pipeline with condition
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_delete");
@@ -355,6 +388,7 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
test("should test different comparison operators (>=, !=, Contains, NotContains)", async ({ page }) => {
const pipelineName = `pipeline-operators-${Math.random().toString(36).substring(7)}`;
currentPipelineName = pipelineName;
await pageManager.pipelinesPage.createPipelineWithCondition("e2e_conditions_operators");
@@ -373,7 +407,20 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => {
await pageManager.pipelinesPage.verifyConditionCount(3);
await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName);
await ingestion(page);
// Ingest test data to streams
const streamNames = [
"e2e_conditions_basic",
"e2e_conditions_groups",
"e2e_conditions_validation",
"e2e_conditions_precedence",
"e2e_conditions_multiple",
"e2e_conditions_delete",
"e2e_conditions_operators"
];
for (const streamName of streamNames) {
await pageManager.logsPage.ingestData(streamName, logsdata.slice(0, 10));
}
await pageManager.pipelinesPage.openPipelineMenu();
await pageManager.pipelinesPage.deletePipelineByName(pipelineName);
@@ -61,7 +61,18 @@ test.describe("Pre-Test Cleanup", () => {
/^stress_test/, // stress_test*, stress_test123, etc.
/^sdr_/, // sdr_* (SDR test streams)
/^e2e_join_/, // e2e_join_* (UNION test streams)
/^[a-z]{8,9}$/ // Random 8-9 char lowercase strings
/^e2e_conditions_/, // e2e_conditions_* (Pipeline conditions UI test streams)
/^[a-z]{8,9}$/, // Random 8-9 char lowercase strings
/^e2e_cond_prec_(src|dest)_\d+$/, // Test 1: Operator precedence test streams
/^e2e_multiple_or_(src|dest)_\d+$/, // Test 2: Multiple OR test streams
/^e2e_nested_or_(src|dest)_\d+$/, // Test 3: Nested OR test streams
/^e2e_numeric_(src|dest)_\d+$/, // Test 4: Numeric comparison test streams
/^e2e_multiple_and_(src|dest)_\d+$/, // Test 5: Multiple AND test streams
/^e2e_deep_(src|dest)_\d+$/, // Test 6: Deeply nested test streams
/^e2e_not_operator_(src|dest)_\d+$/, // Test 7: NOT operator test streams
/^e2e_impossible_(src|dest)_\d+$/, // Test 8: Impossible condition test streams
/^e2e_universal_(src|dest)_\d+$/, // Test 9: Universal condition test streams
/^e2e_4level_(src|dest)_\d+$/ // Test 10: 4-level nested test streams
],
// Protected streams to never delete
['default', 'sensitive', 'important', 'critical', 'production', 'staging', 'automation', 'e2e_automate']