-
Notifications
You must be signed in to change notification settings - Fork 383
Enhance robustness of InternalOpenSearchSink tests through scenario-driven coverage, refactoring, and regression prevention #6146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cwperks
merged 6 commits into
opensearch-project:main
from
pCastq:test/improve-sink-test-coverage-and-structure
Jul 14, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6bf3ea4
test(auditlog): add unit and integration tests for InternalOpenSearch…
pCastq f345baf
Enhance robustness of InternalOpenSearchSink tests through scenario-d…
pCastq b0ed4d2
spotlessApply
pCastq f34cc61
Migrate InternalOpenSearchSinkTest to LuceneTestCase to align with pr…
pCastq 09c4343
Merge branch 'main' into test/improve-sink-test-coverage-and-structure
DarshitChanpura 07b2132
Refactor imports, add audit event to test, and apply spotless and che…
pCastq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
186 changes: 186 additions & 0 deletions
186
.../org/opensearch/security/auditlog/sink/AbstractInternalOpenSearchSinkIntegrationTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| * | ||
| * Modifications Copyright OpenSearch Contributors. See | ||
| * GitHub history for details. | ||
| */ | ||
| package org.opensearch.security.auditlog.sink; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
| import org.opensearch.action.search.SearchRequest; | ||
| import org.opensearch.action.search.SearchResponse; | ||
| import org.opensearch.index.query.QueryBuilders; | ||
| import org.opensearch.search.builder.SearchSourceBuilder; | ||
| import org.opensearch.search.sort.SortOrder; | ||
| import org.opensearch.test.framework.cluster.LocalCluster; | ||
| import org.opensearch.test.framework.cluster.TestRestClient; | ||
| import org.opensearch.transport.client.Client; | ||
|
|
||
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||
| import static org.hamcrest.MatcherAssert.assertThat; | ||
| import static org.hamcrest.Matchers.greaterThan; | ||
| import static org.hamcrest.Matchers.greaterThanOrEqualTo; | ||
| import static org.hamcrest.Matchers.is; | ||
| import static org.awaitility.Awaitility.await; | ||
|
|
||
| /** | ||
| * Base class for {@link InternalOpenSearchSink} integration tests. | ||
| * | ||
| * <p>Holds test methods that are valid regardless of whether the audit target | ||
| * is a plain concrete index or a write alias. Each subclass provides its own | ||
| * {@code @ClassRule} {@link LocalCluster} and overrides | ||
| * {@link #cluster()} and {@link #auditTarget()} to supply the correct | ||
| * cluster and query pattern for that configuration variant.</p> | ||
| * | ||
| * <p>Using a shared base class avoids duplicating {@code testPersistsAuditEventsToTarget} | ||
| * and {@code testAuditDocumentContainsMandatoryFields} across the two concrete | ||
| * integration-test classes that differ only in cluster setup.</p> | ||
| * | ||
| * @see InternalOpenSearchSinkIntegrationTestConcreteIndex concrete date-based index variant | ||
| * @see InternalOpenSearchSinkIntegrationTestAuditAlias write-alias variant | ||
| */ | ||
| abstract class AbstractInternalOpenSearchSinkIntegrationTest { | ||
|
|
||
| /** | ||
| * Returns the cluster configured for this test variant. | ||
| * Implementations must expose the {@code @ClassRule}-annotated cluster field. | ||
| */ | ||
| abstract LocalCluster cluster(); | ||
|
|
||
| /** | ||
| * Returns the index name or alias used to query audit documents. | ||
| * | ||
| * <p>Examples:</p> | ||
| * <ul> | ||
| * <li>{@code "security-auditlog-*"} — default date-based variant</li> | ||
| * <li>{@code "security-audit-write-alias"} — alias variant</li> | ||
| * </ul> | ||
| */ | ||
| abstract String auditTarget(); | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // Shared helpers | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| /** Counts all audit documents reachable through {@link #auditTarget()}. */ | ||
| long countAuditDocs(Client client) { | ||
| return Objects.requireNonNull( | ||
| client.search(new SearchRequest(auditTarget()).source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).size(0))) | ||
| .actionGet() | ||
| .getHits() | ||
| .getTotalHits() | ||
| ).value(); | ||
| } | ||
|
|
||
| /** Refreshes the audit target so newly indexed documents become searchable. */ | ||
| void refreshAuditTarget(Client client) { | ||
| client.admin().indices().prepareRefresh(auditTarget()).get(); | ||
| } | ||
|
|
||
| /** Issues an authenticated REST GET that triggers one {@code GRANTED_PRIVILEGES} audit event. */ | ||
| void generateAuditEvent(String path) { | ||
| try (TestRestClient restClient = cluster().getRestClient(cluster().getAdminCertificate())) { | ||
| restClient.get(path); | ||
| } | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // Shared tests — valid for both date-based index and alias configurations | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Verifies that multiple audit events are successfully persisted to the audit target. | ||
| * | ||
| * <p><b>Tested Code Path:</b> From the second event onward, | ||
| * {@code createIndexIfAbsent()} detects the target already exists | ||
| * (via {@code metadata.hasIndex()} for concrete indices or | ||
| * {@code metadata.hasAlias()} for aliases) and returns {@code true} | ||
| * without attempting recreation. Successful persistence of both events | ||
| * confirms the early-return branch works correctly.</p> | ||
| */ | ||
| @Test | ||
| public void testPersistsAuditEventsToTarget() { | ||
| try (Client client = cluster().getInternalNodeClient()) { | ||
| long before = countAuditDocs(client); | ||
|
|
||
| generateAuditEvent("_cluster/health"); | ||
| generateAuditEvent("_cluster/stats"); | ||
| generateAuditEvent("_nodes"); | ||
|
|
||
| await().atMost(10, SECONDS).pollInterval(200, MILLISECONDS).untilAsserted(() -> { | ||
| refreshAuditTarget(client); | ||
| assertThat("At least 3 new audit events must be persisted", countAuditDocs(client) - before, greaterThanOrEqualTo(3L)); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates that audit documents contain all mandatory fields defined by the | ||
| * audit log specification. | ||
| * | ||
| * <p><b>Core Fields (all events):</b></p> | ||
| * <ul> | ||
| * <li>{@code audit_category} — event classification (e.g., {@code GRANTED_PRIVILEGES})</li> | ||
| * <li>{@code audit_request_layer} — processing layer; expected value: {@code REST}</li> | ||
| * <li>{@code audit_request_origin} — origin layer; expected value: {@code REST}</li> | ||
| * <li>{@code @timestamp} — event timestamp</li> | ||
| * </ul> | ||
| * | ||
| * <p><b>REST-Specific Fields:</b></p> | ||
| * <ul> | ||
| * <li>{@code audit_rest_request_method} — HTTP method (e.g., {@code GET})</li> | ||
| * <li>{@code audit_rest_request_path} — request path (e.g., {@code /_cluster/health})</li> | ||
| * </ul> | ||
| * | ||
| * <p><b>Absent Fields (transport audit disabled):</b></p> | ||
| * <ul> | ||
| * <li>{@code audit_transport_request_type} — must not be present</li> | ||
| * </ul> | ||
| */ | ||
| @Test | ||
| public void testAuditDocumentContainsMandatoryFields() { | ||
| try (Client client = cluster().getInternalNodeClient()) { | ||
| long before = countAuditDocs(client); | ||
|
|
||
| generateAuditEvent("_cluster/health"); | ||
|
|
||
| await().atMost(10, SECONDS).pollInterval(200, MILLISECONDS).untilAsserted(() -> { | ||
| refreshAuditTarget(client); | ||
| assertThat("Test must generate at least one new event", countAuditDocs(client) - before, greaterThan(0L)); | ||
| }); | ||
|
|
||
| SearchResponse response = client.search( | ||
| new SearchRequest(auditTarget()).source( | ||
| new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).size(1).sort("@timestamp", SortOrder.DESC) | ||
| ) | ||
| ).actionGet(); | ||
|
|
||
| assertThat( | ||
| "At least one audit document must exist", | ||
| Objects.requireNonNull(response.getHits().getTotalHits()).value(), | ||
| greaterThan(0L) | ||
| ); | ||
|
|
||
| Map<String, Object> doc = response.getHits().getAt(0).getSourceAsMap(); | ||
|
|
||
| assertThat("Missing field: audit_category", doc.containsKey("audit_category"), is(true)); | ||
| assertThat("Missing field: audit_request_layer", doc.containsKey("audit_request_layer"), is(true)); | ||
| assertThat("Wrong value: audit_request_layer", doc.get("audit_request_layer"), is("REST")); | ||
| assertThat("Missing field: audit_request_origin", doc.containsKey("audit_request_origin"), is(true)); | ||
| assertThat("Wrong value: audit_request_origin", doc.get("audit_request_origin"), is("REST")); | ||
| assertThat("Missing field: @timestamp", doc.containsKey("@timestamp"), is(true)); | ||
| assertThat("Missing field: audit_rest_request_method", doc.containsKey("audit_rest_request_method"), is(true)); | ||
| assertThat("Missing field: audit_rest_request_path", doc.containsKey("audit_rest_request_path"), is(true)); | ||
| assertThat("Unexpected field: audit_transport_request_type", doc.containsKey("audit_transport_request_type"), is(false)); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.