Parent Epic
#35476 — QA EPIC: OpenSearch Migration
Unblocked by
PR #35609 — Vendor-neutral SearchAPI and phase-aware router
Description
Validates the new SearchAPI / SearchAPIImpl layer introduced by PR #35609: phase-aware routing correctness, vendor-neutral DTO structure, Velocity backward-compat bridges, and ContentletAPIInterceptor hook wiring.
Test plan reference: docs/backend/OPENSEARCH_MIGRATION_TEST_PLAN.md — Scenario L, cases L-1–L-10 (TC-026–TC-035)
New Classes Under Test
| Class |
Role |
SearchAPIImpl |
Phase-aware router: ES reads in phases 0–1, OS reads in phases 2–3 |
ESSearchAPIImpl |
ES adapter — converts SearchResponse → neutral DTOs |
OSSearchAPIImpl |
Native OS implementation |
ContentSearchResults<T> |
Typed result list — no unchecked cast required |
ContentSearchResponse |
Neutral raw response DTO |
AggregationBucket |
Neutral terms aggregation bucket |
Test Cases
L-1 (TC-026) — Phase 0: search() delegates to ESSearchAPIImpl
| Step |
Action |
Expected Result |
| 1 |
Start dotCMS in Phase 0. Publish a content item. Perform a content search. |
ContentSearchResults<Contentlet> returned. ESSearchAPIImpl invoked. |
| 2 |
Confirm no OS connection attempt. |
No OS log entries. OSSearchAPIImpl not called. |
L-2 (TC-027) — Phase 1: search() still delegates to ESSearchAPIImpl
| Step |
Action |
Expected Result |
| 1 |
Start dotCMS in Phase 1. Publish a content item. Perform search. |
Results come from ES. ESSearchAPIImpl invoked. No OS read in logs. |
| 2 |
Confirm dual-written OS document is present in OS Dashboards. |
OS has the document (dual-write), but search result came from ES. |
L-3 (TC-028) — Phase 2: search() delegates to OSSearchAPIImpl
| Step |
Action |
Expected Result |
| 1 |
Start dotCMS in Phase 2. Publish a content item. Wait for dual-write. Perform content search. |
Results come from OS. OSSearchAPIImpl invoked. ContentSearchResults<Contentlet> contains the item. |
| 2 |
Confirm no ESSearchAPIImpl invocation for the search read. |
ES not contacted for reads. |
L-4 (TC-029) — Phase 3: search() delegates to OSSearchAPIImpl; no ES contact
| Step |
Action |
Expected Result |
| 1 |
Start dotCMS in Phase 3. Perform a content search. |
OSSearchAPIImpl invoked. Results from OS. No ES connection attempted. |
| 2 |
Stop ES container. Perform search again. |
Search still succeeds. |
L-5 (TC-030) — Phase 2: OS exception on search() triggers ES fallback
Covered by C-8 (#35751) for end-to-end manual validation.
For integration-level coverage: add a unit test to SearchAPIImplTest that injects a mock OSSearchAPIImpl throwing DotDataException and verifies PhaseRouter.readChecked() falls back to ESSearchAPIImpl.
→ Tracked under issue #35669.
L-6 (TC-031) — ContentSearchResults<T> type-safe iteration
| Step |
Action |
Expected Result |
| 1 |
In Phase 1 or 2, retrieve results via ContentletAPI.search(query, false, admin, false). |
Iterates with typed Contentlet objects — no (Contentlet) cast. getTotalResults() returns expected count. getScrollId() is null for non-paginated queries. |
L-7 (TC-032) — searchRaw() aggregations return AggregationBucket map
| Step |
Action |
Expected Result |
| 1 |
In Phase 1, call ContentletAPI.searchRaw() with a terms aggregation on contenttype. |
ContentSearchResponse.aggregations() returns Map<String, List<AggregationBucket>>. Each bucket has .key() and .docCount(). No Elasticsearch-specific types at the call site. |
| 2 |
Run same query in Phase 2. |
Same structure returned from OSSearchAPIImpl. Bucket counts match OS Dashboards. |
L-8 (TC-033) — Velocity $ESContent.search(query) works in Phase 1 and Phase 2
| Step |
Action |
Expected Result |
| 1 |
Render a page with $ESContent.search(query) in Phase 1. |
#foreach($c in $result) iterates ContentMap objects. No ClassCastException. |
| 2 |
Switch to Phase 2. Render same page. |
Same result. ContentMap objects returned. Template unaffected by phase change. |
L-9 (TC-034) — Deprecated ESContentTool.esSearch() bridge returns ESSearchResults
| Step |
Action |
Expected Result |
| 1 |
Call $ESContent.esSearch(query) in a Velocity template in Phase 1. |
Returns ESSearchResults. Page renders. No regression for templates not yet migrated. |
L-10 (TC-035) — ContentletAPIInterceptor fires pre/post hooks on search()
| Step |
Action |
Expected Result |
| 1 |
Register a ContentletAPIPreHook that sets a flag on search(). Call ContentletAPI.search() via APILocator.getContentletAPI(). |
Pre-hook fires. Search returns results. Post-hook fires after return. |
| 2 |
Verify community-license guard on searchRaw(). |
Community edition: DotStateException. Enterprise: result returned normally. |
Acceptance Criteria
Parent Epic
#35476 — QA EPIC: OpenSearch Migration
Unblocked by
PR #35609 — Vendor-neutral SearchAPI and phase-aware router
Description
Validates the new
SearchAPI/SearchAPIImpllayer introduced by PR #35609: phase-aware routing correctness, vendor-neutral DTO structure, Velocity backward-compat bridges, andContentletAPIInterceptorhook wiring.Test plan reference:
docs/backend/OPENSEARCH_MIGRATION_TEST_PLAN.md— Scenario L, cases L-1–L-10 (TC-026–TC-035)New Classes Under Test
SearchAPIImplESSearchAPIImplSearchResponse→ neutral DTOsOSSearchAPIImplContentSearchResults<T>ContentSearchResponseAggregationBucketTest Cases
L-1 (TC-026) — Phase 0:
search()delegates to ESSearchAPIImplContentSearchResults<Contentlet>returned.ESSearchAPIImplinvoked.OSSearchAPIImplnot called.L-2 (TC-027) — Phase 1:
search()still delegates to ESSearchAPIImplESSearchAPIImplinvoked. No OS read in logs.L-3 (TC-028) — Phase 2:
search()delegates to OSSearchAPIImplOSSearchAPIImplinvoked.ContentSearchResults<Contentlet>contains the item.ESSearchAPIImplinvocation for the search read.L-4 (TC-029) — Phase 3:
search()delegates to OSSearchAPIImpl; no ES contactOSSearchAPIImplinvoked. Results from OS. No ES connection attempted.L-5 (TC-030) — Phase 2: OS exception on
search()triggers ES fallbackCovered by C-8 (#35751) for end-to-end manual validation.
For integration-level coverage: add a unit test to
SearchAPIImplTestthat injects a mockOSSearchAPIImplthrowingDotDataExceptionand verifiesPhaseRouter.readChecked()falls back toESSearchAPIImpl.→ Tracked under issue #35669.
L-6 (TC-031) —
ContentSearchResults<T>type-safe iterationContentletAPI.search(query, false, admin, false).Contentletobjects — no(Contentlet)cast.getTotalResults()returns expected count.getScrollId()isnullfor non-paginated queries.L-7 (TC-032) —
searchRaw()aggregations returnAggregationBucketmapContentletAPI.searchRaw()with atermsaggregation oncontenttype.ContentSearchResponse.aggregations()returnsMap<String, List<AggregationBucket>>. Each bucket has.key()and.docCount(). No Elasticsearch-specific types at the call site.OSSearchAPIImpl. Bucket counts match OS Dashboards.L-8 (TC-033) — Velocity
$ESContent.search(query)works in Phase 1 and Phase 2$ESContent.search(query)in Phase 1.#foreach($c in $result)iteratesContentMapobjects. NoClassCastException.ContentMapobjects returned. Template unaffected by phase change.L-9 (TC-034) — Deprecated
ESContentTool.esSearch()bridge returnsESSearchResults$ESContent.esSearch(query)in a Velocity template in Phase 1.ESSearchResults. Page renders. No regression for templates not yet migrated.L-10 (TC-035) —
ContentletAPIInterceptorfires pre/post hooks onsearch()ContentletAPIPreHookthat sets a flag onsearch(). CallContentletAPI.search()viaAPILocator.getContentletAPI().searchRaw().DotStateException. Enterprise: result returned normally.Acceptance Criteria
search()toESSearchAPIImpl; no OS readsearch()toOSSearchAPIImplsearch()toOSSearchAPIImpl; no ES contactContentSearchResults<Contentlet>iterates type-safe; no unchecked castAggregationBucketmap returned from both ES and OS implementations$ESContent.search()renders correctly in Phase 1 and Phase 2$ESContent.esSearch()bridge still functionalContentletAPIInterceptorpre/post hooks fire onsearch()