diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResolutionStatusResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResolutionStatusResource.java index c75e9820e20..0b9a9711a34 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResolutionStatusResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResolutionStatusResource.java @@ -30,12 +30,13 @@ import jakarta.ws.rs.core.UriInfo; import java.beans.IntrospectionException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; import java.util.List; import java.util.UUID; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import org.openmetadata.schema.EntityInterface; import org.openmetadata.schema.api.tests.CreateTestCaseResolutionStatus; +import org.openmetadata.schema.tests.TestCase; import org.openmetadata.schema.tests.type.TestCaseResolutionStatus; import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes; import org.openmetadata.schema.type.Include; @@ -53,9 +54,9 @@ import org.openmetadata.service.security.AuthRequest; import org.openmetadata.service.security.AuthorizationLogic; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.policyevaluator.OperationContext; -import org.openmetadata.service.security.policyevaluator.ResourceContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; import org.openmetadata.service.security.policyevaluator.TestCaseResourceContext; +import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; import org.openmetadata.service.util.FullyQualifiedName; import org.openmetadata.service.util.RestUtil; @@ -159,18 +160,12 @@ public class TestCaseResolutionStatusResource @Parameter(description = "Filter incidents by domain", schema = @Schema(type = "String")) @QueryParam("domain") String domain) { - List requests = new ArrayList<>(); - OperationContext testCaseOperationContext = - new OperationContext(Entity.TEST_CASE, MetadataOperation.VIEW_ALL); ResourceContextInterface testCaseResourceContext = getTestCaseResourceContext(testCaseFQN); - requests.add(new AuthRequest(testCaseOperationContext, testCaseResourceContext)); - if (originEntityFQN != null) { - OperationContext entityOperationContext = - new OperationContext(Entity.TABLE, MetadataOperation.VIEW_TESTS); - ResourceContextInterface entityResourceContext = - new ResourceContext<>(Entity.TABLE, null, originEntityFQN); - requests.add(new AuthRequest(entityOperationContext, entityResourceContext)); - } + ResourceContextInterface entityResourceContext = + buildEntityResourceContext(testCaseFQN, testCaseId, originEntityFQN); + List requests = + buildViewAuthRequests(testCaseResourceContext, entityResourceContext); + authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY); ListFilter filter = new ListFilter(include); @@ -202,10 +197,11 @@ public class TestCaseResolutionStatusResource @Context SecurityContext securityContext, @Parameter(description = "Sequence ID", schema = @Schema(type = "UUID")) @PathParam("stateId") UUID stateId) { - OperationContext testCaseOperationContext = - new OperationContext(Entity.TEST_CASE, MetadataOperation.VIEW_ALL); ResourceContextInterface testCaseResourceContext = TestCaseResourceContext.builder().build(); - authorizer.authorize(securityContext, testCaseOperationContext, testCaseResourceContext); + ResourceContextInterface entityResourceContext = TestCaseResourceContext.builder().build(); + List requests = + buildViewAuthRequests(testCaseResourceContext, entityResourceContext); + authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY); return repository.listTestCaseResolutionStatusesForStateId(stateId); } @@ -230,12 +226,26 @@ public class TestCaseResolutionStatusResource @Parameter(description = "Test Case Failure Status ID", schema = @Schema(type = "UUID")) @PathParam("id") UUID testCaseResolutionStatusId) { - OperationContext testCaseOperationContext = - new OperationContext(Entity.TEST_CASE, MetadataOperation.VIEW_ALL); - ResourceContextInterface testCaseResourceContext = TestCaseResourceContext.builder().build(); - authorizer.authorize(securityContext, testCaseOperationContext, testCaseResourceContext); + TestCaseResolutionStatus testCaseResolutionStatus = + repository.getById(testCaseResolutionStatusId); + TestCase testCase = + Entity.getEntityByName( + Entity.TEST_CASE, + testCaseResolutionStatus.getTestCaseReference().getFullyQualifiedName(), + "", + Include.ALL); - return repository.getById(testCaseResolutionStatusId); + MessageParser.EntityLink entityLink = MessageParser.EntityLink.parse(testCase.getEntityLink()); + + ResourceContextInterface testCaseResourceContext = + TestCaseResourceContext.builder().name(testCase.getFullyQualifiedName()).build(); + ResourceContextInterface entityResourceContext = + TestCaseResourceContext.builder().entityLink(entityLink).build(); + List requests = + buildViewAuthRequests(testCaseResourceContext, entityResourceContext); + authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY); + + return testCaseResolutionStatus; } @POST @@ -256,17 +266,22 @@ public class TestCaseResolutionStatusResource @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestCaseResolutionStatus createTestCaseResolutionStatus) { - OperationContext testCaseOperationContext = - new OperationContext(Entity.TEST_CASE, MetadataOperation.EDIT_TESTS); - ResourceContextInterface testCaseResourceContext = TestCaseResourceContext.builder().build(); - OperationContext entityOperationContext = - new OperationContext(Entity.TABLE, MetadataOperation.EDIT_TESTS); - ResourceContextInterface entityResourceContext = TestCaseResourceContext.builder().build(); + TestCase testCase = + Entity.getEntityByName( + Entity.TEST_CASE, + createTestCaseResolutionStatus.getTestCaseReference(), + "", + Include.ALL); + + MessageParser.EntityLink entityLink = MessageParser.EntityLink.parse(testCase.getEntityLink()); + + ResourceContextInterface testCaseResourceContext = + TestCaseResourceContext.builder().name(testCase.getFullyQualifiedName()).build(); + ResourceContextInterface entityResourceContext = + TestCaseResourceContext.builder().entityLink(entityLink).build(); List requests = - List.of( - new AuthRequest(entityOperationContext, entityResourceContext), - new AuthRequest(testCaseOperationContext, testCaseResourceContext)); + buildEditAuthRequests(testCaseResourceContext, entityResourceContext); authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY); TestCaseResolutionStatus testCaseResolutionStatus = @@ -305,10 +320,25 @@ public class TestCaseResolutionStatusResource })) JsonPatch patch) throws IntrospectionException, InvocationTargetException, IllegalAccessException { - OperationContext testCaseOperationContext = - new OperationContext(Entity.TEST_CASE, MetadataOperation.EDIT_TESTS); - ResourceContextInterface testCaseResourceContext = TestCaseResourceContext.builder().build(); - authorizer.authorize(securityContext, testCaseOperationContext, testCaseResourceContext); + + TestCaseResolutionStatus testCaseResolutionStatus = repository.getById(id); + TestCase testCase = + Entity.getEntityByName( + Entity.TEST_CASE, + testCaseResolutionStatus.getTestCaseReference().getFullyQualifiedName(), + "", + Include.ALL); + + MessageParser.EntityLink entityLink = MessageParser.EntityLink.parse(testCase.getEntityLink()); + + ResourceContextInterface testCaseResourceContext = + TestCaseResourceContext.builder().name(testCase.getFullyQualifiedName()).build(); + ResourceContextInterface entityResourceContext = + TestCaseResourceContext.builder().entityLink(entityLink).build(); + List requests = + buildEditAuthRequests(testCaseResourceContext, entityResourceContext); + + authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY); RestUtil.PatchResponse response = repository.patch(id, patch, securityContext.getUserPrincipal().getName()); return response.toResponse(); @@ -420,11 +450,19 @@ public class TestCaseResolutionStatusResource searchListFilter.addQueryParam("originEntityFQN", originEntityFQN); searchListFilter.addQueryParam("domains", domain); - OperationContext testCaseOperationContext = - new OperationContext(Entity.TEST_CASE, MetadataOperation.VIEW_ALL); - ResourceContextInterface testCaseResourceContext = getTestCaseResourceContext(testCaseFQN); + ResourceContextInterface testCaseResourceContext = TestCaseResourceContext.builder().build(); + ResourceContextInterface entityResourceContext = + buildEntityResourceContext(testCaseFQN, null, originEntityFQN); + List requests = + List.of( + new AuthRequest( + new OperationContext(Entity.TEST_CASE, MetadataOperation.VIEW_ALL), + testCaseResourceContext), + new AuthRequest( + new OperationContext(Entity.TABLE, MetadataOperation.VIEW_ALL), + entityResourceContext)); - authorizer.authorize(securityContext, testCaseOperationContext, testCaseResourceContext); + authorizer.authorizeRequests(securityContext, requests, AuthorizationLogic.ANY); if (latest) { // For latest results, use aggregation grouped by test case to get the latest status per test @@ -436,17 +474,8 @@ public class TestCaseResolutionStatusResource // case null); } else { - return super.listInternalFromSearch( - securityContext, - new Fields(null), - searchListFilter, - limit, - offset, - searchSortFilter, - null, - null, - testCaseOperationContext, - testCaseResourceContext); + return repository.listFromSearchWithOffset( + new Fields(null), searchListFilter, limit, offset, searchSortFilter, null, null); } } @@ -471,4 +500,58 @@ public class TestCaseResolutionStatusResource } return resourceContext; } + + protected static List buildViewAuthRequests( + ResourceContextInterface testCaseResourceContext, + ResourceContextInterface entityResourceContext) { + return List.of( + new AuthRequest( + new OperationContext(Entity.TEST_CASE, MetadataOperation.VIEW_ALL), + testCaseResourceContext), + new AuthRequest( + new OperationContext(Entity.TABLE, MetadataOperation.VIEW_ALL), entityResourceContext), + new AuthRequest( + new OperationContext(Entity.TABLE, MetadataOperation.VIEW_TESTS), + entityResourceContext)); + } + + protected static List buildEditAuthRequests( + ResourceContextInterface testCaseResourceContext, + ResourceContextInterface entityResourceContext) { + return List.of( + new AuthRequest( + new OperationContext(Entity.TABLE, MetadataOperation.EDIT_TESTS), + entityResourceContext), + new AuthRequest( + new OperationContext(Entity.TABLE, MetadataOperation.EDIT_ALL), entityResourceContext), + new AuthRequest( + new OperationContext(Entity.TEST_CASE, MetadataOperation.EDIT_TESTS), + testCaseResourceContext), + new AuthRequest( + new OperationContext(Entity.TEST_CASE, MetadataOperation.EDIT_ALL), + testCaseResourceContext)); + } + + protected static ResourceContextInterface buildEntityResourceContext( + String testCaseFQN, UUID testCaseId, String originEntityFQN) { + if (testCaseFQN != null) { + TestCase testCase = Entity.getEntityByName(Entity.TEST_CASE, testCaseFQN, "", Include.ALL); + MessageParser.EntityLink entityLink = + MessageParser.EntityLink.parse(testCase.getEntityLink()); + return TestCaseResourceContext.builder().entityLink(entityLink).build(); + } else if (testCaseId != null) { + TestCase testCase = Entity.getEntity(Entity.TEST_CASE, testCaseId, "", Include.ALL); + MessageParser.EntityLink entityLink = + MessageParser.EntityLink.parse(testCase.getEntityLink()); + return TestCaseResourceContext.builder().entityLink(entityLink).build(); + } else if (originEntityFQN != null) { + EntityInterface entityInterface = + Entity.getEntityByName(Entity.TABLE, originEntityFQN, "", Include.ALL); + String entityLinkStr = + EntityUtil.buildEntityLink(Entity.TABLE, entityInterface.getFullyQualifiedName()); + MessageParser.EntityLink entityLink = MessageParser.EntityLink.parse(entityLinkStr); + return TestCaseResourceContext.builder().entityLink(entityLink).build(); + } + return TestCaseResourceContext.builder().build(); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java index ead14900767..a89949983ab 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/EntityUtil.java @@ -242,6 +242,10 @@ public final class EntityUtil { return Entity.getEntityReferenceByName(entityType, fqn, ALL); } + public static String buildEntityLink(String entityType, String fullyQualifiedName) { + return String.format("<#E::%s::%s>", entityType, fullyQualifiedName); + } + public static UsageDetails getLatestUsage(UsageDAO usageDAO, UUID entityId) { LOG.debug("Getting latest usage for {}", entityId); UsageDetails details = usageDAO.getLatestUsage(entityId.toString()); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java index 6cb77a1374e..c4c65b598f4 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java @@ -1631,7 +1631,7 @@ public class TestCaseResourceTest extends EntityResourceTest queryParams = new HashMap<>(); - queryParams.put("testCaseFQN", TEST_TABLE1.getFullyQualifiedName()); + queryParams.put("testCaseFQN", testCaseEntity2.getFullyQualifiedName()); storedTestCaseResolutions = getTestCaseFailureStatus(startTs, endTs, null, null, queryParams); assertTrue( storedTestCaseResolutions.getData().stream() @@ -1639,7 +1639,7 @@ public class TestCaseResourceTest extends EntityResourceTest t.getTestCaseReference() .getFullyQualifiedName() - .equals(testCaseEntity1.getFullyQualifiedName()))); + .equals(testCaseEntity2.getFullyQualifiedName()))); // Get the test case resolution by origin entity FQN queryParams.clear(); @@ -1654,8 +1654,10 @@ public class TestCaseResourceTest extends EntityResourceTest getTestCaseFailureStatus(startTs, endTs, null, null, queryParams), + NOT_FOUND, + "table instance for IDONOTEXIST123 not found"); // Delete test case recursively and check that the test case resolution status is also deleted // 1. soft delete - should not delete the test case resolution status @@ -1744,32 +1746,91 @@ public class TestCaseResourceTest extends EntityResourceTest", table.getFullyQualifiedName())) + .withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName()) + .withParameterValues( + List.of(new TestCaseParameterValue().withValue("100").withName("maxValue"))) + .withOwners(List.of(USER_TABLE_OWNER.getEntityReference())); + TestCase testCaseEntity = createAndCheckEntity(createTestCase, ADMIN_AUTH_HEADERS); + CreateTestCaseResolutionStatus createTestCaseFailureStatus = new CreateTestCaseResolutionStatus() .withTestCaseReference(testCaseEntity.getFullyQualifiedName()) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Ack) .withSeverity(Severity.Severity2) .withTestCaseResolutionStatusDetails(null); + + // Test 1: Admin can create incident status TestCaseResolutionStatus testCaseFailureStatus = createTestCaseFailureStatus(createTestCaseFailureStatus); + + // Test 2: USER_TABLE_OWNER (owner with EDIT_TESTS via isOwner() policy) can patch String original = JsonUtils.pojoToJson(testCaseFailureStatus); String updated = JsonUtils.pojoToJson( testCaseFailureStatus .withUpdatedAt(System.currentTimeMillis()) - .withUpdatedBy(USER1_REF) + .withUpdatedBy(USER_TABLE_OWNER.getEntityReference()) .withSeverity(Severity.Severity1)); JsonNode patch = TestUtils.getJsonPatch(original, updated); TestCaseResolutionStatus patched = - patchTestCaseResultFailureStatus(testCaseFailureStatus.getId(), patch); + patchTestCaseResultFailureStatus( + testCaseFailureStatus.getId(), patch, authHeaders(USER_TABLE_OWNER.getName())); TestCaseResolutionStatus stored = getTestCaseFailureStatus(testCaseFailureStatus.getId()); // check our patch fields have been updated assertEquals(patched.getUpdatedAt(), stored.getUpdatedAt()); assertEquals(patched.getUpdatedBy(), stored.getUpdatedBy()); - assertEquals(patched.getSeverity(), stored.getSeverity()); + assertEquals(Severity.Severity1, stored.getSeverity()); + + // Test 3: USER_NO_PERMISSIONS cannot patch (not owner, no EDIT_ALL) + String updatedForNoPerms = + JsonUtils.pojoToJson( + stored + .withUpdatedAt(System.currentTimeMillis()) + .withUpdatedBy(USER_NO_PERMISSIONS.getEntityReference()) + .withSeverity(Severity.Severity3)); + JsonNode patchForNoPerms = + TestUtils.getJsonPatch(JsonUtils.pojoToJson(stored), updatedForNoPerms); + assertResponse( + () -> + patchTestCaseResultFailureStatus( + stored.getId(), patchForNoPerms, authHeaders(USER_NO_PERMISSIONS.getName())), + FORBIDDEN, + "User does not have ANY of the required permissions."); + + // Test 4: CREATE_ALL_OPS_USER (with EDIT_ALL on TEST_CASE) can patch even if not owner + String updatedForAllOps = + JsonUtils.pojoToJson( + stored + .withUpdatedAt(System.currentTimeMillis()) + .withUpdatedBy(CREATE_ALL_OPS_USER.getEntityReference()) + .withSeverity(Severity.Severity4)); + JsonNode patchForAllOps = + TestUtils.getJsonPatch( + JsonUtils.pojoToJson( + stored.withSeverity( + Severity.Severity3)), // restore to previous state as modified ln 1823 + updatedForAllOps); + TestCaseResolutionStatus patchedByAllOps = + patchTestCaseResultFailureStatus( + stored.getId(), patchForAllOps, authHeaders(CREATE_ALL_OPS_USER.getName())); + assertEquals(Severity.Severity4, patchedByAllOps.getSeverity()); } @Test @@ -2140,6 +2201,216 @@ public class TestCaseResourceTest extends EntityResourceTest", table.getFullyQualifiedName())) + .withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName()) + .withParameterValues( + List.of(new TestCaseParameterValue().withValue("100").withName("maxValue"))); + TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + + // Add a failed test result to create an incident + postTestCaseResult( + testCase.getFullyQualifiedName(), + new CreateTestCaseResult() + .withResult("failed") + .withTestCaseStatus(TestCaseStatus.Failed) + .withTimestamp(TestUtils.dateToTimestamp("2024-01-01")), + ADMIN_AUTH_HEADERS); + + CreateTestCaseResolutionStatus createIncident = + new CreateTestCaseResolutionStatus() + .withTestCaseReference(testCase.getFullyQualifiedName()) + .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) + .withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF)); + + // Test 1: User with EDIT_TESTS permission on Table should be able to create incident + TestCaseResolutionStatus incident1 = + createTestCaseFailureStatus(createIncident, authHeaders(USER_TABLE_EDIT_TESTS.getName())); + assertNotNull(incident1); + assertEquals( + TestCaseResolutionStatusTypes.Assigned, incident1.getTestCaseResolutionStatusType()); + + // Test 2: User with EDIT_TESTS permission on Table should be able to update incident + String original1 = JsonUtils.pojoToJson(incident1); + String updated1 = + JsonUtils.pojoToJson( + incident1 + .withUpdatedAt(System.currentTimeMillis()) + .withUpdatedBy(USER_TABLE_EDIT_TESTS.getEntityReference()) + .withSeverity(Severity.Severity1)); + JsonNode patch1 = TestUtils.getJsonPatch(original1, updated1); + TestCaseResolutionStatus patched1 = + patchTestCaseResultFailureStatus( + incident1.getId(), patch1, authHeaders(USER_TABLE_EDIT_TESTS.getName())); + assertEquals(Severity.Severity1, patched1.getSeverity()); + + // Test 3: User with EDIT_ALL permission on TestCase should be able to create incident + CreateTestCaseResolutionStatus createIncident2 = + new CreateTestCaseResolutionStatus() + .withTestCaseReference(testCase.getFullyQualifiedName()) + .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved) + .withTestCaseResolutionStatusDetails(new Resolved()); + + TestCaseResolutionStatus incident2 = + createTestCaseFailureStatus(createIncident2, authHeaders(USER_TEST_CASE_UPDATE.getName())); + assertNotNull(incident2); + assertEquals( + TestCaseResolutionStatusTypes.Resolved, incident2.getTestCaseResolutionStatusType()); + + // Test 4: User with EDIT_ALL permission on TestCase should be able to update incident + String original2 = JsonUtils.pojoToJson(incident2); + String updated2 = + JsonUtils.pojoToJson( + incident2 + .withUpdatedAt(System.currentTimeMillis()) + .withUpdatedBy(USER_TEST_CASE_UPDATE.getEntityReference()) + .withSeverity(Severity.Severity2)); + JsonNode patch2 = TestUtils.getJsonPatch(original2, updated2); + TestCaseResolutionStatus patched2 = + patchTestCaseResultFailureStatus( + incident2.getId(), patch2, authHeaders(USER_TEST_CASE_UPDATE.getName())); + assertEquals(Severity.Severity2, patched2.getSeverity()); + + // Test 5: User with ALL permissions should be able to create incident + TestCaseResolutionStatus incident3 = + createTestCaseFailureStatus(createIncident, authHeaders(CREATE_ALL_OPS_USER.getName())); + assertNotNull(incident3); + + // Test 6: User with ALL permissions should be able to update incident + String original3 = JsonUtils.pojoToJson(incident3); + String updated3 = + JsonUtils.pojoToJson( + incident3 + .withUpdatedAt(System.currentTimeMillis()) + .withUpdatedBy(CREATE_ALL_OPS_USER.getEntityReference()) + .withSeverity(Severity.Severity3)); + JsonNode patch3 = TestUtils.getJsonPatch(original3, updated3); + TestCaseResolutionStatus patched3 = + patchTestCaseResultFailureStatus( + incident3.getId(), patch3, authHeaders(CREATE_ALL_OPS_USER.getName())); + assertEquals(Severity.Severity3, patched3.getSeverity()); + + // Test 7: User without required permissions should NOT be able to create incident + assertThrows( + HttpResponseException.class, + () -> + createTestCaseFailureStatus(createIncident, authHeaders(USER_NO_PERMISSIONS.getName())), + "User without permissions should not be able to create incident status"); + + // Test 8: User without required permissions should NOT be able to update incident + String originalNoPerms = JsonUtils.pojoToJson(incident1); + String updatedNoPerms = + JsonUtils.pojoToJson( + incident1 + .withUpdatedAt(System.currentTimeMillis()) + .withUpdatedBy(USER_NO_PERMISSIONS.getEntityReference()) + .withSeverity(Severity.Severity4)); + JsonNode patchNoPerms = TestUtils.getJsonPatch(originalNoPerms, updatedNoPerms); + + assertThrows( + HttpResponseException.class, + () -> + patchTestCaseResultFailureStatus( + incident1.getId(), patchNoPerms, authHeaders(USER_NO_PERMISSIONS.getName())), + "User without permissions should not be able to update incident status"); + } + + @Test + void test_getTestCaseResolutionStatusPermissions(TestInfo test) + throws IOException, ParseException { + // Create a test case to test TestCaseResolutionStatus GET permissions + CreateTestCase create = createRequest(test); + create + .withEntityLink(TABLE_LINK) + .withTestDefinition(TEST_DEFINITION4.getFullyQualifiedName()) + .withParameterValues( + List.of(new TestCaseParameterValue().withValue("100").withName("maxValue"))); + TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + + // Add a failed test result to create an incident + postTestCaseResult( + testCase.getFullyQualifiedName(), + new CreateTestCaseResult() + .withResult("failed") + .withTestCaseStatus(TestCaseStatus.Failed) + .withTimestamp(TestUtils.dateToTimestamp("2024-01-01")), + ADMIN_AUTH_HEADERS); + + // Create a test case resolution status (incident) + CreateTestCaseResolutionStatus createIncident = + new CreateTestCaseResolutionStatus() + .withTestCaseReference(testCase.getFullyQualifiedName()) + .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) + .withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF)); + TestCaseResolutionStatus incident = + createTestCaseFailureStatus(createIncident, ADMIN_AUTH_HEADERS); + + // Test GET by ID endpoint permissions for TestCaseResolutionStatus + // Admin should be able to retrieve test case resolution status + TestCaseResolutionStatus retrievedIncident1 = + getTestCaseFailureStatus(incident.getId(), ADMIN_AUTH_HEADERS); + assertNotNull(retrievedIncident1); + + // Data consumer should be able to view test case resolution status (has VIEW permissions) + TestCaseResolutionStatus retrievedIncident2 = + getTestCaseFailureStatus(incident.getId(), authHeaders(DATA_CONSUMER.getName())); + assertNotNull(retrievedIncident2); + + // Data steward should be able to view test case resolution status (has VIEW permissions) + TestCaseResolutionStatus retrievedIncident3 = + getTestCaseFailureStatus(incident.getId(), authHeaders(DATA_STEWARD.getName())); + assertNotNull(retrievedIncident3); + + // Test GET list endpoint permissions for TestCaseResolutionStatus with testCaseFQN parameter + // Admin should be able to list test case resolution statuses + ResultList listResult1 = + listTestCaseFailureStatusWithFQN(testCase.getFullyQualifiedName(), ADMIN_AUTH_HEADERS); + assertNotNull(listResult1); + + // Data consumer should be able to list test case resolution statuses + ResultList listResult2 = + listTestCaseFailureStatusWithFQN( + testCase.getFullyQualifiedName(), authHeaders(DATA_CONSUMER.getName())); + assertNotNull(listResult2); + + // Data steward should be able to list test case resolution statuses + ResultList listResult3 = + listTestCaseFailureStatusWithFQN( + testCase.getFullyQualifiedName(), authHeaders(DATA_STEWARD.getName())); + assertNotNull(listResult3); + + // Test GET list endpoint permissions for TestCaseResolutionStatus with testCaseId parameter + // Admin should be able to list test case resolution statuses by test case ID + ResultList listResult4 = + listTestCaseFailureStatusWithId(testCase.getId(), ADMIN_AUTH_HEADERS); + assertNotNull(listResult4); + + // Data consumer should be able to list test case resolution statuses by test case ID + ResultList listResult5 = + listTestCaseFailureStatusWithId(testCase.getId(), authHeaders(DATA_CONSUMER.getName())); + assertNotNull(listResult5); + + // Data steward should be able to list test case resolution statuses by test case ID + ResultList listResult6 = + listTestCaseFailureStatusWithId(testCase.getId(), authHeaders(DATA_STEWARD.getName())); + assertNotNull(listResult6); + } + @Test void wrongMinMaxTestParameter(TestInfo test) throws HttpResponseException { CreateTestCase validTestCase = createRequest(test); @@ -3670,19 +3941,27 @@ public class TestCaseResourceTest extends EntityResourceTest authHeaders) + throws HttpResponseException { WebTarget target = getCollection().path("/testCaseIncidentStatus"); return TestUtils.post( - target, - createTestCaseFailureStatus, - TestCaseResolutionStatus.class, - 200, - ADMIN_AUTH_HEADERS); + target, createTestCaseFailureStatus, TestCaseResolutionStatus.class, 200, authHeaders); } private TestCaseResolutionStatus patchTestCaseResultFailureStatus( UUID testCaseFailureStatusId, JsonNode patch) throws HttpResponseException { + return patchTestCaseResultFailureStatus(testCaseFailureStatusId, patch, ADMIN_AUTH_HEADERS); + } + + private TestCaseResolutionStatus patchTestCaseResultFailureStatus( + UUID testCaseFailureStatusId, JsonNode patch, Map authHeaders) + throws HttpResponseException { WebTarget target = getCollection().path("/testCaseIncidentStatus/" + testCaseFailureStatusId); - return TestUtils.patch(target, patch, TestCaseResolutionStatus.class, ADMIN_AUTH_HEADERS); + return TestUtils.patch(target, patch, TestCaseResolutionStatus.class, authHeaders); } private TestCaseResolutionStatus getTestCaseFailureStatus(UUID testCaseFailureStatusId) @@ -4645,4 +4924,49 @@ public class TestCaseResourceTest extends EntityResourceTest listTestCaseFailureStatusWithFQN( + String testCaseFQN, Map authHeaders) throws HttpResponseException { + WebTarget target = + getResource("dataQuality/testCases/testCaseIncidentStatus") + .queryParam("testCaseFQN", testCaseFQN) + .queryParam("startTs", 0) + .queryParam("endTs", System.currentTimeMillis()); + return TestUtils.get( + target, + TestCaseResolutionStatusResource.TestCaseResolutionStatusResultList.class, + authHeaders); + } + + private ResultList listTestCaseFailureStatusWithId( + UUID testCaseId, Map authHeaders) throws HttpResponseException { + WebTarget target = + getResource("dataQuality/testCases/testCaseIncidentStatus") + .queryParam("testCaseId", testCaseId) + .queryParam("startTs", 0) + .queryParam("endTs", System.currentTimeMillis()); + return TestUtils.get( + target, + TestCaseResolutionStatusResource.TestCaseResolutionStatusResultList.class, + authHeaders); + } + + private ResultList listTestCaseFailureStatusWithOriginFQN( + String originEntityFQN, Map authHeaders) throws HttpResponseException { + WebTarget target = + getResource("dataQuality/testCases/testCaseIncidentStatus") + .queryParam("originEntityFQN", originEntityFQN) + .queryParam("startTs", 0) + .queryParam("endTs", System.currentTimeMillis()); + return TestUtils.get( + target, + TestCaseResolutionStatusResource.TestCaseResolutionStatusResultList.class, + authHeaders); + } + + private TestCaseResolutionStatus getTestCaseFailureStatus( + UUID id, Map authHeaders) throws HttpResponseException { + WebTarget target = getResource("dataQuality/testCases/testCaseIncidentStatus/" + id); + return TestUtils.get(target, TestCaseResolutionStatus.class, authHeaders); + } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts index e4358e8fca7..7ae9db43dbe 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts @@ -10,20 +10,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import test, { expect } from '@playwright/test'; +import { expect } from '@playwright/test'; import { get } from 'lodash'; import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config'; import { SidebarItem } from '../../constant/sidebar'; +import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; import { TableClass } from '../../support/entity/TableClass'; import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; import { resetTokenFromBotPage } from '../../utils/bot'; import { clickOutside, - createNewPage, descriptionBox, getApiContext, redirectToHomePage, } from '../../utils/common'; +import { addOwner } from '../../utils/entity'; import { acknowledgeTask, assignIncident, @@ -32,6 +34,7 @@ import { } from '../../utils/incidentManager'; import { makeRetryRequest } from '../../utils/serviceIngestion'; import { sidebarClick } from '../../utils/sidebar'; +import { test } from '../fixtures/pages'; const user1 = new UserClass(); const user2 = new UserClass(); @@ -39,9 +42,6 @@ const user3 = new UserClass(); const users = [user1, user2, user3]; const table1 = new TableClass(); -// use the admin user to login -test.use({ storageState: 'playwright/.auth/admin.json' }); - test.describe.configure({ mode: 'serial' }); test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => { @@ -49,7 +49,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => { // since we need to poll for the pipeline status, we need to increase the timeout test.slow(); - const { afterAction, apiContext, page } = await createNewPage(browser); + const { afterAction, apiContext, page } = await performAdminLogin(browser); if (!process.env.PLAYWRIGHT_IS_OSS) { // Todo: Remove this patch once the issue is fixed #19140 @@ -88,7 +88,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => { }); test.afterAll(async ({ browser }) => { - const { apiContext, afterAction } = await createNewPage(browser); + const { apiContext, afterAction } = await performAdminLogin(browser); for (const entity of [...users, table1]) { await entity.delete(apiContext); } @@ -101,7 +101,10 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => { await redirectToHomePage(page); }); - test('Basic Scenario', async ({ page }) => { + test('Complete Incident lifecycle with table owner', async ({ + page: adminPage, + ownerPage: page, + }) => { const testCase = table1.testCasesResponseData[0]; const testCaseName = testCase?.['name']; const assignee = { @@ -109,6 +112,31 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => { displayName: user1.getUserName(), }; + await test.step('Claim ownership of table', async () => { + const loggedInUserRequest = page.waitForResponse( + `/api/v1/users/loggedInUser*` + ); + await redirectToHomePage(page); + const loggedInUserResponse = await loggedInUserRequest; + const loggedInUser = await loggedInUserResponse.json(); + + await redirectToHomePage(adminPage); + + await table1.visitEntityPage(adminPage); + await adminPage.waitForLoadState('networkidle'); + await adminPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await addOwner({ + page: adminPage, + owner: loggedInUser.displayName, + type: 'Users', + endpoint: EntityTypeEndpoint.Table, + dataTestId: 'data-assets-header', + }); + }); + await test.step("Acknowledge table test case's failure", async () => { await acknowledgeTask({ page,