Files
openobserve/tests/ui-testing/playwright-tests/SDR/queryTimeHash.spec.js
T
Shrinath RaoandClaude Opus 4.8 c178de4013 fix(test): SDR combined-field verification via search API (definitive fix for e2e/SDR flakes) (#12896)
## Problem

The 7 `@combined` SDR specs (`queryTime{Redaction,Drop,Hash}`,
`ingestionTime{Redaction,Hash,Drop}`, `multipleSDRPatterns`) fail
deterministically on `e2e / SDR` (ENT [run
28216495512](https://github.com/openobserve/o2-enterprise/actions/runs/28216495512))
with `Field <name> should be visible but was not found`, even after the
content-gate retry in #12893.

## Root cause (a logic bug, not timing)

Verification scraped the virtualized logs results table. Two structural
defects:

1. **Row-indexed selector misused as a count.**
`[data-test="log-table-column-0-source"]` — `TenstackTable.vue` renders
`log-table-column-{rowIndex}-source`, so it only ever matches **row 0**
(1 element). The combined path's `count >= 4` gate can never be met by
the reliable source locator.
2. **Dead fallback selector.** `.logs-result-table tbody tr[role="row"]`
references a class that **does not exist** in `web/src` → always 0.

So the fetch collapsed to whole-page `tbody tr` scraping whose text
never matched the field keys → the flat *"4 entries found but fields not
yet indexed"* across every retry. #12893's retry couldn't help because
the scraped data was structurally wrong, not late. Single-field tests
passed only because they read row 0 directly.

## Fix

Verify against the **Search API** — the actual source of truth for SDR.
Query-time redaction is applied server-side in `apply_regex_to_response`
(`ApplyPolicy::AtSearch`); the web only displays API output, so
`[REDACTED]` / `[REDACTED:<hash>]` / dropped-field semantics are all
present in the API hits.

- Each ingest tags records with a unique `sdr_test_id` marker and
returns it.
- Verification queries `_search` with `WHERE sdr_test_id = '<marker>'`,
polls only until the **known** record count is searchable, then asserts
on the JSON hits.
- Markers threaded through all 7 specs: query-time reuses one marker
(visible → transformed); ingestion-time uses distinct per-batch markers;
`multipleSDRPatterns` threads one per sequential ingest.

Net **−80 lines** — removes all brittle DOM scraping from SDR
verification.

## Validation

`node --check` passes on all changed files; every verify call confirmed
to thread a marker; root-cause selectors + server-side redaction path
confirmed against source. Live `e2e / SDR` run validated via a
matching-name ENT branch (CI tree-merge picks up this OSS branch).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 06:40:30 +00:00

165 lines
6.7 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');
test.describe("Query Time Hash - Combined Test", { tag: '@enterprise' }, () => {
test.describe.configure({ mode: 'serial' });
let pm;
// Generate unique test run ID for isolation
const testRunId = Date.now().toString(36);
// All patterns used in this spec file - with full pattern definitions for uniqueness
const patternsToTest = [
{
name: `email_format_query_hash_${testRunId}`,
description: 'Email address validation pattern (for query time hash tests)',
pattern: '^[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,}$',
field: 'user_email',
value: 'john.doe@example.com'
},
{
name: `us_phone_query_hash_${testRunId}`,
description: 'US phone number (10 digits) (for query time hash tests)',
pattern: '^\\d{10}$',
field: 'phone',
value: '5551234567'
},
{
name: `credit_card_query_hash_${testRunId}`,
description: 'Credit card number pattern (for query time hash tests)',
pattern: '^\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}$',
field: 'cc_number',
value: '1234 5678 9012 3456'
},
{
name: `ssn_query_hash_${testRunId}`,
description: 'Social Security Number pattern (for query time hash tests)',
pattern: '^\\d{3}-\\d{2}-\\d{4}$',
field: 'ssn',
value: '123-45-6789'
}
];
// Use unique stream name to avoid "stream being deleted" conflicts
const testStreamName = `sdr_query_hash_${testRunId}`;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
testLogger.info('Combined query time hash test setup completed');
});
test("setup: cleanup patterns", {
tag: ['@sdr', '@cleanup', '@queryHash']
}, async ({ page }) => {
testLogger.info('=== SPEC-LEVEL CLEANUP: Cleaning up patterns for combined query hash test ===');
for (const pattern of patternsToTest) {
testLogger.info(`Checking and cleaning up pattern: ${pattern.name}`);
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.checkPatternExists(pattern.name);
if (exists) {
testLogger.info(`Pattern ${pattern.name} exists. Deleting (and unlinking if needed).`);
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(pattern.name);
if (!deleteResult.success && deleteResult.reason === 'in_use') {
testLogger.info('Pattern in use, unlinking from streams first');
for (const association of deleteResult.associations) {
await pm.streamAssociationPage.unlinkPatternFromField(
association.streamName,
association.fieldName,
pattern.name
);
}
// Delete again after unlinking
await pm.sdrPatternsPage.navigateToRegexPatterns();
await pm.sdrPatternsPage.deletePatternByName(pattern.name);
}
testLogger.info(`✓ Pattern ${pattern.name} deleted successfully`);
} else {
testLogger.info(`✓ Pattern ${pattern.name} does not exist, no cleanup needed`);
}
}
testLogger.info('=== SPEC-LEVEL CLEANUP COMPLETE ===');
});
test("should hash multiple fields at query time when patterns are applied", {
tag: ['@sdr', '@queryHash', '@all', '@combined']
}, async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'dependency', description: 'setup: cleanup patterns' });
testLogger.info('=== Testing combined HASH for 4 patterns at query time ===');
// STEP 1: Ingest data once (data will remain unchanged in storage)
testLogger.info('STEP 1: Ingest 4 lines of data (before any patterns are applied)');
const dataToIngest = patternsToTest.map(p => ({
fieldName: p.field,
fieldValue: p.value
}));
// Query-time SDR transforms data at search time, so STEP 1 (visible) and STEP 4
// (hashed) inspect the SAME ingested batch — reuse one marker for both.
const ingestMarker = await pm.logsPage.ingestMultipleFields(testStreamName, dataToIngest);
// Verify all fields are visible without hashing
const fieldsBeforeHashing = patternsToTest.map(p => ({
fieldName: p.field,
shouldBeHashed: false
}));
await pm.sdrVerificationPage.verifyMultipleFields(pm.logsPage, testStreamName, fieldsBeforeHashing, ingestMarker);
testLogger.info('✓ STEP 1 PASSED: All fields visible without SDR');
// STEP 2: Create all 4 SDR patterns
testLogger.info('STEP 2: Create all 4 SDR patterns');
for (const patternConfig of patternsToTest) {
await pm.sdrPatternsPage.navigateToRegexPatterns();
await pm.sdrPatternsPage.createPattern(patternConfig.name, patternConfig.description, patternConfig.pattern);
await pm.sdrPatternsPage.verifyPatternCreatedSuccess();
// Verify pattern was created
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.checkPatternExists(patternConfig.name);
expect(exists).toBeTruthy();
testLogger.info(`✓ Created and verified pattern: ${patternConfig.name}`);
}
testLogger.info('✓ STEP 2 PASSED: All 4 patterns created and verified');
// STEP 3: Link all 4 patterns to their respective fields with QUERY TIME hashing
testLogger.info('STEP 3: Link all 4 patterns to their respective fields with HASH action');
for (const patternConfig of patternsToTest) {
await pm.streamAssociationPage.associatePatternWithStream(
testStreamName,
patternConfig.name,
'hash',
'query', // QUERY TIME - key difference from ingestion time
patternConfig.field
);
testLogger.info(`✓ Linked pattern ${patternConfig.name} to field ${patternConfig.field} with HASH action`);
}
testLogger.info('✓ STEP 3 PASSED: All 4 patterns linked to fields with HASH action');
// STEP 4: Verify all fields are HASHED at query time (NO RE-INGESTION)
testLogger.info('STEP 4: Verify fields are HASHED at query time (data unchanged in storage)');
const fieldsAfterHashing = patternsToTest.map(p => ({
fieldName: p.field,
shouldBeHashed: true
}));
await pm.sdrVerificationPage.verifyMultipleFields(pm.logsPage, testStreamName, fieldsAfterHashing, ingestMarker);
testLogger.info('✓ STEP 4 PASSED: All fields are HASHED at query time');
testLogger.info('=== ✓ COMBINED QUERY TIME HASH TEST COMPLETED SUCCESSFULLY ===');
});
});