Files
openobserve/tests/ui-testing/playwright-tests/SDR/multipleSDRPatterns.spec.js
T

454 lines
20 KiB
JavaScript

const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const PageManager = require('../../pages/page-manager.js');
const path = require('path');
// Helper function to ingest a SINGLE log entry
async function ingestSingleLog(page, streamName, fieldName, fieldValue) {
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 logEntry = {
level: "info",
[fieldName]: fieldValue,
log: `Test log with ${fieldName} = ${fieldValue}`,
_timestamp: Date.now() * 1000
};
testLogger.info(`Ingesting single log with ${fieldName}: ${fieldValue}`);
const response = await page.evaluate(async ({ url, headers, orgId, streamName, logData }) => {
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
method: 'POST',
headers: headers,
body: JSON.stringify([logData])
});
const responseJson = await fetchResponse.json();
return {
status: fetchResponse.status,
statusText: fetchResponse.statusText,
body: responseJson
};
}, {
url: process.env.INGESTION_URL,
headers: headers,
orgId: orgId,
streamName: streamName,
logData: logEntry
});
testLogger.info(`Ingestion API response - Status: ${response.status}, Body:`, response.body);
if (response.status !== 200) {
testLogger.error(`Ingestion failed! Status: ${response.status}, Response:`, response.body);
}
await page.waitForTimeout(2000);
}
async function closeStreamDetailSidebar(page) {
const cancelButton = page.getByRole('button', { name: 'Cancel' });
const cancelVisible = await cancelButton.isVisible({ timeout: 1000 }).catch(() => false);
if (cancelVisible) {
await cancelButton.click();
testLogger.info('Closed stream detail sidebar');
await page.waitForTimeout(500);
}
}
async function navigateToLogsQuick(page) {
await page.locator('[data-test="menu-link-\\/logs-item"]').click();
await page.waitForLoadState('networkidle');
testLogger.info('Navigated to Logs (fast - no VRL wait)');
}
// Verify a single field in the most recent log entry
async function verifySingleFieldInLatestLog(page, pm, streamName, fieldName, shouldBeDropped, shouldBeRedacted) {
testLogger.info(`Verifying field: ${fieldName}, shouldBeDropped: ${shouldBeDropped}, shouldBeRedacted: ${shouldBeRedacted}`);
await closeStreamDetailSidebar(page);
await navigateToLogsQuick(page);
await pm.logsPage.selectStream(streamName);
await page.waitForTimeout(1000);
await pm.logsPage.clickRefreshButton();
await page.waitForTimeout(2000);
// Get log entries
let logTableCell = page.locator('[data-test="log-table-column-0-source"]');
let logCount = await logTableCell.count();
if (logCount === 0) {
logTableCell = page.locator('tbody tr');
logCount = await logTableCell.count();
}
testLogger.info(`Found ${logCount} log entries in the UI`);
if (logCount === 0) {
throw new Error('No logs found in the stream');
}
// Get the first log entry (most recent)
const logText = await logTableCell.nth(0).textContent();
testLogger.info(`Latest log text (first 300 chars): ${logText.substring(0, 300)}...`);
// Check if field exists in the log
const fieldAsJsonKey = `"${fieldName}":`;
const fieldAsKey = `${fieldName}:`;
const fieldFound = logText.includes(fieldAsJsonKey) || logText.includes(fieldAsKey);
if (shouldBeDropped) {
// Field should NOT be present at all
if (fieldFound) {
testLogger.error(`Field ${fieldName} should have been DROPPED but was found in log!`);
throw new Error(`Field ${fieldName} was not dropped - still present in logs`);
}
testLogger.info(`✓ Field ${fieldName} is correctly DROPPED (not present)`);
} else if (shouldBeRedacted) {
// Field should be present with [REDACTED]
if (!fieldFound) {
testLogger.error(`Field ${fieldName} should be present but was not found in log!`);
throw new Error(`Field ${fieldName} not found in log`);
}
if (!logText.includes('[REDACTED]')) {
testLogger.error(`Field ${fieldName} should be REDACTED but [REDACTED] marker not found!`);
throw new Error(`Field ${fieldName} was not redacted`);
}
testLogger.info(`✓ Field ${fieldName} is correctly REDACTED`);
} else {
// Field should be visible with actual value
if (!fieldFound) {
testLogger.error(`Field ${fieldName} should be visible but was not found in log!`);
throw new Error(`Field ${fieldName} not found in log`);
}
if (logText.includes('[REDACTED]')) {
testLogger.error(`Field ${fieldName} should be visible but is REDACTED!`);
throw new Error(`Field ${fieldName} is unexpectedly redacted`);
}
testLogger.info(`✓ Field ${fieldName} is visible with actual value (as expected)`);
}
}
test.describe("Import POC - Multiple Patterns on One Field", { tag: '@enterprise' }, () => {
test.describe.configure({ mode: 'serial' });
let pm;
let cleanupPerformedInSetup = false;
// All 6 patterns from import file (for cleanup purposes)
const allImportedPatterns = [
'ifsc_code',
'date_dd_mm_yyyy',
'log_filename',
'time_hh_mm_ss',
'log_level_keyword',
'hex_number'
];
// Query-time patterns (2) - tested first, no re-ingestion needed
const queryTimePatterns = [
{ name: 'log_filename', value: 'application.log', action: 'drop', timeType: 'query', scenario: 'query_drop' },
{ name: 'time_hh_mm_ss', value: '14:30:45', action: 'redact', timeType: 'query', scenario: 'query_redact' }
];
// Ingestion-time patterns (2) - tested second, requires re-ingestion
const ingestionTimePatterns = [
{ name: 'ifsc_code', value: 'AB12CDEF1234567890', action: 'drop', timeType: 'ingestion', scenario: 'ingestion_drop' },
{ name: 'date_dd_mm_yyyy', value: '25/12/2024', action: 'redact', timeType: 'ingestion', scenario: 'ingestion_redact' }
];
const fieldName = "multi_data"; // Single field for all patterns
const testStreamName = "sdr_poc_multi_pattern_test";
const importFilePath = path.resolve(__dirname, '../../../test-data/regex_patterns_import.json');
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
await page.waitForLoadState('networkidle');
testLogger.info('Import POC test setup completed');
});
// TEST 1: Setup cleanup
test('setup: cleanup imported patterns', {
tag: ['@sdr', '@cleanup', '@import', '@poc']
}, async ({ page }) => {
testLogger.info('=== SETUP CLEANUP: Cleaning up ALL 6 imported patterns ===');
// Our 4 test patterns that are linked to the same field
const testPatterns = [...queryTimePatterns.map(p => p.name), ...ingestionTimePatterns.map(p => p.name)];
let needsUnlinking = false;
// STEP 1: Try to delete all patterns first
testLogger.info('STEP 1: Attempting to delete all patterns');
for (const patternName of allImportedPatterns) {
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.checkPatternExists(patternName);
if (exists) {
cleanupPerformedInSetup = true;
testLogger.info(`Pattern ${patternName} exists, attempting delete...`);
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(patternName);
if (deleteResult.success) {
testLogger.info(`✓ Pattern ${patternName} deleted successfully`);
} else if (deleteResult.reason === 'in_use') {
testLogger.info(`Pattern ${patternName} is in use, will need to unlink`);
needsUnlinking = true;
// Don't try to unlink yet, just mark that we need to
}
} else {
testLogger.info(`✓ Pattern ${patternName} does not exist`);
}
}
// STEP 2: If any patterns are in use, unlink all test patterns at once
if (needsUnlinking) {
testLogger.info('STEP 2: Some patterns in use, unlinking our 4 test patterns from field');
await pm.streamAssociationPage.unlinkAllPatternsFromField(
testStreamName,
fieldName,
testPatterns
);
testLogger.info('✓ Unlinking complete');
// STEP 3: Now delete the patterns that were in use
testLogger.info('STEP 3: Deleting patterns after unlinking');
for (const patternName of allImportedPatterns) {
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.checkPatternExists(patternName);
if (exists) {
testLogger.info(`Deleting pattern: ${patternName}`);
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(patternName);
if (deleteResult.success) {
testLogger.info(`✓ Pattern ${patternName} deleted successfully`);
} else if (deleteResult.reason === 'in_use') {
// Handle non-test patterns that might still be linked elsewhere
testLogger.warn(`Pattern ${patternName} still in use (non-test pattern), unlinking individually`);
for (const association of deleteResult.associations) {
await pm.streamAssociationPage.unlinkPatternFromField(
association.streamName,
association.fieldName,
patternName
);
}
await pm.sdrPatternsPage.navigateToRegexPatterns();
const retryDelete = await pm.sdrPatternsPage.deletePatternByName(patternName);
expect(retryDelete.success).toBeTruthy();
testLogger.info(`✓ Pattern ${patternName} deleted after unlinking`);
} else {
throw new Error(`Failed to delete pattern ${patternName}. Reason: ${deleteResult.reason}`);
}
}
}
}
testLogger.info(`=== SETUP CLEANUP COMPLETE (Cleanup performed: ${cleanupPerformedInSetup}) ===`);
});
// TEST 2: Import patterns and verify
test('should import 4 patterns and verify they exist', {
tag: ['@sdr', '@import', '@poc']
}, async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'dependency', description: 'setup: cleanup imported patterns' });
testLogger.info('=== Testing pattern import functionality ===');
await pm.sdrPatternsPage.navigateToRegexPatterns();
// Import patterns
testLogger.info('Importing patterns from file');
const importSuccess = await pm.sdrPatternsPage.importPatternsFromFile(importFilePath);
expect(importSuccess).toBeTruthy();
// Verify each imported pattern exists by searching for it
testLogger.info('Verifying all 4 patterns exist by searching for them');
await pm.sdrPatternsPage.navigateToRegexPatterns();
await page.waitForTimeout(1000);
// Combine both pattern arrays to verify all 4
const allPatternsToVerify = [...queryTimePatterns, ...ingestionTimePatterns];
for (const pattern of allPatternsToVerify) {
testLogger.info(`Searching for pattern: ${pattern.name}`);
const exists = await pm.sdrPatternsPage.verifyPatternExistsInList(pattern.name);
expect(exists).toBeTruthy();
testLogger.info(`✓ Pattern ${pattern.name} found in list`);
}
testLogger.info('=== ✓ IMPORT TEST COMPLETED SUCCESSFULLY ===');
});
// TEST 3: Multiple patterns on ONE field (SEQUENTIAL FLOW)
test('should link 4 patterns to one field and verify with sequential ingestion', {
tag: ['@sdr', '@import', '@poc', '@multiPattern']
}, async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'dependency', description: 'should import 4 patterns and verify they exist' });
testLogger.info('=== Testing multiple patterns on ONE field - SEQUENTIAL FLOW ===');
testLogger.info(`Field: ${fieldName}`);
testLogger.info('Strategy: Ingest one log → verify → ingest next → verify (repeating pattern)');
// ==================== STEP 1: Ingest log #1 for query-time drop pattern ====================
testLogger.info('========== STEP 1: Ingest log with value "application.log" ==========');
await ingestSingleLog(page, testStreamName, fieldName, queryTimePatterns[0].value); // "application.log"
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, false);
testLogger.info('✓ STEP 1 PASSED: Log #1 ingested, field visible (no patterns yet)');
// ==================== STEP 2: Link query-time DROP pattern ====================
testLogger.info('========== STEP 2: Link query-time DROP pattern (log_filename) ==========');
await pm.streamAssociationPage.associatePatternWithStream(
testStreamName,
queryTimePatterns[0].name, // 'log_filename'
queryTimePatterns[0].action, // 'drop'
queryTimePatterns[0].timeType, // 'query'
fieldName
);
testLogger.info('✓ STEP 2 PASSED: Query-time DROP pattern linked');
// ==================== STEP 3: Verify query-time DROP on existing log ====================
testLogger.info('========== STEP 3: Verify query-time DROP works on EXISTING log ==========');
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, true, false);
testLogger.info('✓ STEP 3 PASSED: Field DROPPED at query time (no re-ingestion needed)');
// ==================== STEP 4: Ingest log #2 for query-time redact pattern ====================
testLogger.info('========== STEP 4: Ingest log with value "14:30:45" ==========');
await ingestSingleLog(page, testStreamName, fieldName, queryTimePatterns[1].value); // "14:30:45"
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, false);
testLogger.info('✓ STEP 4 PASSED: Log #2 ingested, field visible (pattern not yet linked)');
// ==================== STEP 5: Link query-time REDACT pattern ====================
testLogger.info('========== STEP 5: Link query-time REDACT pattern (time_hh_mm_ss) ==========');
await pm.streamAssociationPage.addAdditionalPatternToField(
testStreamName,
fieldName,
queryTimePatterns[1].name, // 'time_hh_mm_ss'
queryTimePatterns[1].action, // 'redact'
queryTimePatterns[1].timeType // 'query'
);
testLogger.info('✓ STEP 5 PASSED: Query-time REDACT pattern linked (now 2 patterns total)');
// ==================== STEP 6: Verify query-time REDACT on existing log ====================
testLogger.info('========== STEP 6: Verify query-time REDACT works on EXISTING log ==========');
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, true);
testLogger.info('✓ STEP 6 PASSED: Field REDACTED at query time (no re-ingestion needed)');
// ==================== STEP 7: Link ingestion-time DROP pattern ====================
testLogger.info('========== STEP 7: Link ingestion-time DROP pattern (ifsc_code) ==========');
await pm.streamAssociationPage.addAdditionalPatternToField(
testStreamName,
fieldName,
ingestionTimePatterns[0].name, // 'ifsc_code'
ingestionTimePatterns[0].action, // 'drop'
ingestionTimePatterns[0].timeType // 'ingestion'
);
testLogger.info('✓ STEP 7 PASSED: Ingestion-time DROP pattern linked (now 3 patterns total)');
// ==================== STEP 8: Ingest log #3 and verify ingestion-time DROP ====================
testLogger.info('========== STEP 8: Ingest log with value "AB12CDEF1234567890" ==========');
await ingestSingleLog(page, testStreamName, fieldName, ingestionTimePatterns[0].value); // IFSC code
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, true, false);
testLogger.info('✓ STEP 8 PASSED: Field DROPPED at ingestion time');
// ==================== STEP 9: Link ingestion-time REDACT pattern ====================
testLogger.info('========== STEP 9: Link ingestion-time REDACT pattern (date_dd_mm_yyyy) ==========');
await pm.streamAssociationPage.addAdditionalPatternToField(
testStreamName,
fieldName,
ingestionTimePatterns[1].name, // 'date_dd_mm_yyyy'
ingestionTimePatterns[1].action, // 'redact'
ingestionTimePatterns[1].timeType // 'ingestion'
);
testLogger.info('✓ STEP 9 PASSED: Ingestion-time REDACT pattern linked (now 4 patterns total on ONE field)');
// ==================== STEP 10: Ingest log #4 and verify ingestion-time REDACT ====================
testLogger.info('========== STEP 10: Ingest log with value "25/12/2024" ==========');
await ingestSingleLog(page, testStreamName, fieldName, ingestionTimePatterns[1].value); // Date
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, true);
testLogger.info('✓ STEP 10 PASSED: Field REDACTED at ingestion time');
testLogger.info('=== ✓ ALL 4 PATTERNS ON ONE FIELD TEST COMPLETED SUCCESSFULLY ===');
testLogger.info(' - Field: multi_data has 4 patterns');
testLogger.info(' - Query-time: 1 drop + 1 redact (work on existing data)');
testLogger.info(' - Ingestion-time: 1 drop + 1 redact (require re-ingestion)');
testLogger.info(' - Each pattern tested independently with sequential ingestion');
});
// TEST 4: End cleanup
test('cleanup: unlink and delete patterns', {
tag: ['@sdr', '@cleanup', '@import', '@poc']
}, async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'dependency', description: 'should link 4 patterns to one field and verify with sequential ingestion' });
testLogger.info('=== END CLEANUP: Unlinking and deleting ALL 6 patterns ===');
// First, unlink ALL patterns from the field in one go (since all 4 test patterns are on the same field)
testLogger.info(`Unlinking all patterns from field ${fieldName} in stream ${testStreamName}`);
await pm.streamAssociationPage.unlinkAllPatternsFromField(testStreamName, fieldName);
testLogger.info('✓ All patterns unlinked from field');
// Now delete each pattern
for (const patternName of allImportedPatterns) {
testLogger.info(`Checking and deleting pattern: ${patternName}`);
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.checkPatternExists(patternName);
if (exists) {
testLogger.info(`Pattern ${patternName} exists. Deleting...`);
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(patternName);
if (deleteResult.success) {
testLogger.info(`✓ Pattern ${patternName} deleted successfully`);
} else if (deleteResult.reason === 'in_use') {
// This shouldn't happen since we already unlinked, but handle it just in case
testLogger.warn(`Pattern ${patternName} still in use. Checking associations...`);
for (const association of deleteResult.associations) {
testLogger.info(`Unlinking from stream: ${association.streamName}, field: ${association.fieldName}`);
await pm.streamAssociationPage.unlinkAllPatternsFromField(
association.streamName,
association.fieldName
);
}
// Try deleting again with retry logic
await pm.sdrPatternsPage.navigateToRegexPatterns();
const finalDelete = await pm.sdrPatternsPage.deletePatternByName(patternName);
if (!finalDelete.success) {
testLogger.warn(`Pattern ${patternName} still in use after unlinking, waiting and retrying...`);
await page.waitForTimeout(2000);
await pm.sdrPatternsPage.navigateToRegexPatterns();
const retryDelete = await pm.sdrPatternsPage.deletePatternByName(patternName);
expect(retryDelete.success).toBeTruthy();
testLogger.info(`✓ Pattern ${patternName} deleted successfully on retry`);
} else {
testLogger.info(`✓ Pattern ${patternName} deleted successfully after unlinking`);
}
} else {
throw new Error(`Failed to delete pattern ${patternName}. Reason: ${deleteResult.reason}`);
}
} else {
testLogger.info(`✓ Pattern ${patternName} does not exist, no cleanup needed`);
}
}
// Verify all patterns have been removed
testLogger.info('Verifying all patterns have been removed');
await pm.sdrPatternsPage.navigateToRegexPatterns();
await page.waitForTimeout(1000);
for (const patternName of allImportedPatterns) {
const exists = await pm.sdrPatternsPage.verifyPatternExistsInList(patternName);
expect(exists).toBeFalsy();
testLogger.info(`✓ Verified pattern ${patternName} is removed`);
}
testLogger.info('=== END CLEANUP COMPLETE ===');
});
});