Skip to content

[QA-G10] OpenSearch Migration — SearchAPI Phase Routing & Neutral DTOs (TC-026–TC-035) #35752

@fabrizzio-dotCMS

Description

@fabrizzio-dotCMS

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

  • L-1, L-2: Phase 0/1 routes search() to ESSearchAPIImpl; no OS read
  • L-3: Phase 2 routes search() to OSSearchAPIImpl
  • L-4: Phase 3 routes search() to OSSearchAPIImpl; no ES contact
  • L-5: Phase 2 OS exception results in ES fallback (unit test in SearchAPIImplTest — see test(search): add SearchAPIImpl phase-routing and permission-filtering integration tests #35669)
  • L-6: ContentSearchResults<Contentlet> iterates type-safe; no unchecked cast
  • L-7: AggregationBucket map returned from both ES and OS implementations
  • L-8: Velocity $ESContent.search() renders correctly in Phase 1 and Phase 2
  • L-9: Deprecated $ESContent.esSearch() bridge still functional
  • L-10: ContentletAPIInterceptor pre/post hooks fire on search()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions