From 5ac2d43fcde6779db0fcd57580caea3ce8435f81 Mon Sep 17 00:00:00 2001 From: nixonrodrigues Date: Fri, 24 Jul 2026 19:50:51 +0530 Subject: [PATCH] ATLAS-5352 :- Add Unit Testcases for Relationship --- .../simple/AtlasSimpleAuthorizerTest.java | 94 +++++++ .../resources/atlas-simple-authz-policy.json | 20 +- .../AtlasRelationshipStoreV2ReadAuthTest.java | 239 ++++++++++++++++++ .../rest/SearchDownloadFileValidatorTest.java | 136 ++++++++++ 4 files changed, 487 insertions(+), 2 deletions(-) create mode 100644 repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2ReadAuthTest.java create mode 100644 webapp/src/test/java/org/apache/atlas/web/rest/SearchDownloadFileValidatorTest.java diff --git a/authorization/src/test/java/org/apache/atlas/authorize/simple/AtlasSimpleAuthorizerTest.java b/authorization/src/test/java/org/apache/atlas/authorize/simple/AtlasSimpleAuthorizerTest.java index d61ae7afca5..2dd67706535 100644 --- a/authorization/src/test/java/org/apache/atlas/authorize/simple/AtlasSimpleAuthorizerTest.java +++ b/authorization/src/test/java/org/apache/atlas/authorize/simple/AtlasSimpleAuthorizerTest.java @@ -50,6 +50,12 @@ public class AtlasSimpleAuthorizerTest { private static final String USER_FINANCE_PII = "financePII"; private static final String USER_IN_ADMIN_GROUP = "admin-group-user"; private static final String USER_IN_UNKNOWN_GROUP = "unknown-group-user"; + private static final String USER_REL_READER = "relreader"; + private static final String USER_RANGER_TAG_SYNC = "rangertagsync"; + + private static final String SALES_FACT_TABLE_QN = "sales_fact@cl1"; + private static final String SALES_FACT_SALES_COLUMN_QN = "Sales.sales_fact.sales@cl1"; + private static final String SALES_FACT_TIME_COLUMN_QN = "Sales.sales_fact.time_id@cl1"; private static final Map> USER_GROUPS = new HashMap<>(); private static final List ENTITY_PRIVILEGES = new ArrayList<>(); @@ -336,6 +342,92 @@ public void testBusinessMetadata() { } } + @Test + public void testRelationshipRead_AllowsWhenWildcardEntityIdsPermitBothEnds() { + assertRelationshipReadAllowed(USER_RANGER_TAG_SYNC, SALES_FACT_TABLE_QN, SALES_FACT_SALES_COLUMN_QN); + } + + @Test + public void testRelationshipRead_AllowsWhenPartialEntityIdsPermitBothEnds() { + assertRelationshipReadAllowed(USER_REL_READER, SALES_FACT_TABLE_QN, SALES_FACT_SALES_COLUMN_QN); + } + + @Test + public void testRelationshipRead_DeniesWhenPartialEntityIdsPermitOnlyTableEnd() { + assertEntityReadAllowed(USER_REL_READER, "Table", SALES_FACT_TABLE_QN); + assertEntityReadDenied(USER_REL_READER, "Column", SALES_FACT_TIME_COLUMN_QN); + assertRelationshipReadDenied(USER_REL_READER, SALES_FACT_TABLE_QN, SALES_FACT_TIME_COLUMN_QN); + } + + @Test + public void testRelationshipRead_DeniesWhenPartialEntityIdsPermitOnlyColumnEnd() { + assertEntityReadDenied(USER_REL_READER, "Table", "other_table@cl1"); + assertEntityReadAllowed(USER_REL_READER, "Column", SALES_FACT_SALES_COLUMN_QN); + assertRelationshipReadDenied(USER_REL_READER, "other_table@cl1", SALES_FACT_SALES_COLUMN_QN); + } + + @Test + public void testRelationshipRead_DeniesWhenUserHasNoEntityRead() { + assertEntityReadDenied(USER_IN_UNKNOWN_GROUP, "Table", SALES_FACT_TABLE_QN); + assertEntityReadDenied(USER_IN_UNKNOWN_GROUP, "Column", SALES_FACT_SALES_COLUMN_QN); + assertRelationshipReadDenied(USER_IN_UNKNOWN_GROUP, SALES_FACT_TABLE_QN, SALES_FACT_SALES_COLUMN_QN); + } + + private void assertRelationshipReadAllowed(String userName, String end1QualifiedName, String end2QualifiedName) { + assertEntityReadAllowed(userName, "Table", end1QualifiedName); + assertEntityReadAllowed(userName, "Column", end2QualifiedName); + + AssertJUnit.assertTrue("user " + userName + " should be allowed relationship read on " + + end1QualifiedName + " and " + end2QualifiedName, + isRelationshipReadAllowed(userName, end1QualifiedName, end2QualifiedName)); + } + + private void assertRelationshipReadDenied(String userName, String end1QualifiedName, String end2QualifiedName) { + AssertJUnit.assertFalse("user " + userName + " should be denied relationship read on " + + end1QualifiedName + " and " + end2QualifiedName, + isRelationshipReadAllowed(userName, end1QualifiedName, end2QualifiedName)); + } + + private void assertEntityReadAllowed(String userName, String typeName, String qualifiedName) { + AssertJUnit.assertTrue("user " + userName + " should have entity-read on " + qualifiedName, + isEntityReadAllowed(userName, typeName, qualifiedName)); + } + + private void assertEntityReadDenied(String userName, String typeName, String qualifiedName) { + AssertJUnit.assertFalse("user " + userName + " should not have entity-read on " + qualifiedName, + isEntityReadAllowed(userName, typeName, qualifiedName)); + } + + private boolean isRelationshipReadAllowed(String userName, String end1QualifiedName, String end2QualifiedName) { + return isEntityReadAllowed(userName, "Table", end1QualifiedName) + && isEntityReadAllowed(userName, "Column", end2QualifiedName); + } + + private boolean isEntityReadAllowed(String userName, String typeName, String qualifiedName) { + try { + AtlasEntityAccessRequest request = new AtlasEntityAccessRequest(null, AtlasPrivilege.ENTITY_READ, + createEntityHeader(typeName, qualifiedName)); + + setUser(request, userName); + + return authorizer.isAccessAllowed(request); + } catch (Exception e) { + LOG.error("Exception in AtlasSimpleAuthorizerTest", e); + + AssertJUnit.fail(); + + return false; + } + } + + private AtlasEntityHeader createEntityHeader(String typeName, String qualifiedName) { + Map attributes = new HashMap<>(); + + attributes.put("qualifiedName", qualifiedName); + + return new AtlasEntityHeader(typeName, attributes); + } + private void setUser(AtlasAccessRequest request, String userName) { Set userGroups = USER_GROUPS.get(userName); @@ -351,6 +443,8 @@ private void setUser(AtlasAccessRequest request, String userName) { USER_GROUPS.put(USER_FINANCE_PII, Collections.singleton("FINANCE_PII")); USER_GROUPS.put(USER_IN_ADMIN_GROUP, Collections.singleton("ROLE_ADMIN")); USER_GROUPS.put(USER_IN_UNKNOWN_GROUP, Collections.singleton("UNKNOWN_GROUP")); + USER_GROUPS.put(USER_REL_READER, Collections.singleton("REL_PARTIAL_READ")); + USER_GROUPS.put(USER_RANGER_TAG_SYNC, Collections.singleton("RANGER_TAG_SYNC")); ENTITY_PRIVILEGES.add(AtlasPrivilege.ENTITY_CREATE); ENTITY_PRIVILEGES.add(AtlasPrivilege.ENTITY_UPDATE); diff --git a/authorization/src/test/resources/atlas-simple-authz-policy.json b/authorization/src/test/resources/atlas-simple-authz-policy.json index cada904470d..f4a254cbbed 100644 --- a/authorization/src/test/resources/atlas-simple-authz-policy.json +++ b/authorization/src/test/resources/atlas-simple-authz-policy.json @@ -127,6 +127,20 @@ "classifications": [ "PII.*" ] } ] + }, + + "REL_PARTIAL_READ": { + "entityPermissions": [ + { + "privileges": [ "entity-read", "entity-read-classification" ], + "entityTypes": [ ".*" ], + "entityIds": [ "sales_fact@cl1", "Sales.sales_fact.sales@cl1" ], + "entityClassifications": [ ".*" ], + "labels": [ ".*" ], + "businessMetadata": [ ".*" ], + "attributes": [ ".*" ] + } + ] } }, @@ -134,7 +148,8 @@ "admin": [ "ROLE_ADMIN" ], "rangertagsync": [ "DATA_SCIENTIST" ], "dataScientist1": [ "DATA_SCIENTIST"], - "dataSteward1": [ "DATA_STEWARD"] + "dataSteward1": [ "DATA_STEWARD"], + "relreader": [ "REL_PARTIAL_READ" ] }, "groupRoles": { @@ -145,6 +160,7 @@ "FINANCE": [ "FINANCE" ], "FINANCE_PII": [ "FINANCE_PII" ], "RANGER_TAG_SYNC": [ "DATA_SCIENTIST" ], - "DATA_STEWARD_EX": [ "DATA_STEWARD_EX" ] + "DATA_STEWARD_EX": [ "DATA_STEWARD_EX" ], + "REL_PARTIAL_READ":[ "REL_PARTIAL_READ" ] } } diff --git a/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2ReadAuthTest.java b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2ReadAuthTest.java new file mode 100644 index 00000000000..b9107feb4d6 --- /dev/null +++ b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2ReadAuthTest.java @@ -0,0 +1,239 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE + * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.apache.atlas.repository.store.graph.v2; + +import org.apache.atlas.AtlasErrorCode; +import org.apache.atlas.authorize.AtlasAuthorizationUtils; +import org.apache.atlas.authorize.AtlasEntityAccessRequest; +import org.apache.atlas.authorize.AtlasPrivilege; +import org.apache.atlas.exception.AtlasBaseException; +import org.apache.atlas.model.instance.AtlasEntityHeader; +import org.apache.atlas.model.instance.AtlasRelationship; +import org.apache.atlas.model.instance.AtlasRelationship.AtlasRelationshipWithExtInfo; +import org.apache.atlas.repository.graph.GraphHelper; +import org.apache.atlas.repository.graphdb.AtlasEdge; +import org.apache.atlas.repository.graphdb.AtlasGraph; +import org.apache.atlas.repository.graphdb.AtlasVertex; +import org.apache.atlas.repository.store.graph.v1.DeleteHandlerDelegate; +import org.apache.atlas.type.AtlasTypeRegistry; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class AtlasRelationshipStoreV2ReadAuthTest { + private static final String RELATIONSHIP_GUID = "relationship-guid-1"; + private static final String END1_GUID = "end1-guid"; + private static final String END2_GUID = "end2-guid"; + private static final String END1_QUALIFIED_NAME = "sales_fact@cl1"; + private static final String END2_QUALIFIED_NAME = "Sales.sales_fact.sales@cl1"; + + @Mock + private AtlasGraph graph; + + @Mock + private AtlasTypeRegistry typeRegistry; + + @Mock + private DeleteHandlerDelegate deleteDelegate; + + @Mock + private IAtlasEntityChangeNotifier entityChangeNotifier; + + @Mock + private GraphHelper graphHelper; + + @Mock + private EntityGraphRetriever entityRetriever; + + @Mock + private AtlasEdge edge; + + @Mock + private AtlasVertex end1Vertex; + + @Mock + private AtlasVertex end2Vertex; + + private AtlasRelationshipStoreV2 relationshipStore; + + private AutoCloseable mocks; + + @BeforeMethod + public void setUp() throws Exception { + mocks = MockitoAnnotations.openMocks(this); + + relationshipStore = new AtlasRelationshipStoreV2(graph, typeRegistry, deleteDelegate, entityChangeNotifier); + + setField("graphHelper", graphHelper); + setField("entityRetriever", entityRetriever); + + when(graphHelper.getEdgeForGUID(RELATIONSHIP_GUID)).thenReturn(edge); + when(edge.getOutVertex()).thenReturn(end1Vertex); + when(edge.getInVertex()).thenReturn(end2Vertex); + + when(entityRetriever.toAtlasEntityHeaderWithClassifications(end1Vertex)).thenReturn(tableHeader(END1_QUALIFIED_NAME)); + when(entityRetriever.toAtlasEntityHeaderWithClassifications(end2Vertex)).thenReturn(columnHeader(END2_QUALIFIED_NAME)); + } + + @AfterMethod + public void tearDown() throws Exception { + if (mocks != null) { + mocks.close(); + } + } + + @Test + public void testGetById_VerifiesEntityReadOnBothEnds() throws Exception { + AtlasRelationship expectedRelationship = new AtlasRelationship("hive_table_columns"); + + when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(expectedRelationship); + + try (MockedStatic authUtils = mockStatic(AtlasAuthorizationUtils.class)) { + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any())) + .thenAnswer(invocation -> null); + + AtlasRelationship actualRelationship = relationshipStore.getById(RELATIONSHIP_GUID); + + assertEquals(actualRelationship, expectedRelationship); + authUtils.verify(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()), times(2)); + } + } + + @Test + public void testGetExtInfoById_VerifiesEntityReadOnBothEnds() throws Exception { + AtlasRelationshipWithExtInfo expectedRelationship = new AtlasRelationshipWithExtInfo(new AtlasRelationship("hive_table_columns")); + + when(entityRetriever.mapEdgeToAtlasRelationshipWithExtInfo(edge)).thenReturn(expectedRelationship); + + try (MockedStatic authUtils = mockStatic(AtlasAuthorizationUtils.class)) { + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any())) + .thenAnswer(invocation -> null); + + AtlasRelationshipWithExtInfo actualRelationship = relationshipStore.getExtInfoById(RELATIONSHIP_GUID); + + assertEquals(actualRelationship, expectedRelationship); + authUtils.verify(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()), times(2)); + } + } + + @Test(expectedExceptions = AtlasBaseException.class) + public void testGetById_DeniesWhenEnd1EntityReadFails() throws Exception { + when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("hive_table_columns")); + + try (MockedStatic authUtils = mockStatic(AtlasAuthorizationUtils.class)) { + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), eq("read relationship: end1 guid="), eq(END1_GUID))) + .thenThrow(new AtlasBaseException(AtlasErrorCode.UNAUTHORIZED_ACCESS, END1_GUID, AtlasPrivilege.ENTITY_READ.getType())); + + relationshipStore.getById(RELATIONSHIP_GUID); + } + } + + @Test(expectedExceptions = AtlasBaseException.class) + public void testGetById_DeniesWhenEnd2EntityReadFails() throws Exception { + when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("hive_table_columns")); + + try (MockedStatic authUtils = mockStatic(AtlasAuthorizationUtils.class)) { + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), eq("read relationship: end1 guid="), eq(END1_GUID))) + .thenAnswer(invocation -> null); + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), eq("read relationship: end2 guid="), eq(END2_GUID))) + .thenThrow(new AtlasBaseException(AtlasErrorCode.UNAUTHORIZED_ACCESS, END2_GUID, AtlasPrivilege.ENTITY_READ.getType())); + + relationshipStore.getById(RELATIONSHIP_GUID); + } + } + + @Test + public void testGetById_PassesEndQualifiedNamesToEntityReadCheck() throws Exception { + when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("Table_Columns")); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AtlasEntityAccessRequest.class); + + try (MockedStatic authUtils = mockStatic(AtlasAuthorizationUtils.class)) { + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(requestCaptor.capture(), any(), any())) + .thenAnswer(invocation -> null); + + relationshipStore.getById(RELATIONSHIP_GUID); + + List requests = requestCaptor.getAllValues(); + + assertEquals(requests.size(), 2); + assertEquals(requests.get(0).getEntityId(), END1_QUALIFIED_NAME); + assertEquals(requests.get(1).getEntityId(), END2_QUALIFIED_NAME); + assertEquals(requests.get(0).getAction(), AtlasPrivilege.ENTITY_READ); + assertEquals(requests.get(1).getAction(), AtlasPrivilege.ENTITY_READ); + } + } + + @Test + public void testGetById_AllowsWhenBothEndQualifiedNamesPermitted() throws Exception { + when(entityRetriever.mapEdgeToAtlasRelationship(edge)).thenReturn(new AtlasRelationship("Table_Columns")); + + try (MockedStatic authUtils = mockStatic(AtlasAuthorizationUtils.class)) { + authUtils.when(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any())) + .thenAnswer(invocation -> { + AtlasEntityAccessRequest request = invocation.getArgument(0); + String entityId = request.getEntityId(); + + assertTrue(entityId.equals(END1_QUALIFIED_NAME) || entityId.equals(END2_QUALIFIED_NAME), + "unexpected entityId for relationship read: " + entityId); + + return null; + }); + + AtlasRelationship actualRelationship = relationshipStore.getById(RELATIONSHIP_GUID); + + assertEquals(actualRelationship.getTypeName(), "Table_Columns"); + authUtils.verify(() -> AtlasAuthorizationUtils.verifyAccess(any(AtlasEntityAccessRequest.class), any(), any()), times(2)); + } + } + + private AtlasEntityHeader tableHeader(String qualifiedName) { + return createEntityHeader("Table", END1_GUID, qualifiedName); + } + + private AtlasEntityHeader columnHeader(String qualifiedName) { + return createEntityHeader("Column", END2_GUID, qualifiedName); + } + + private AtlasEntityHeader createEntityHeader(String typeName, String guid, String qualifiedName) { + Map attributes = new HashMap<>(); + + attributes.put("qualifiedName", qualifiedName); + + return new AtlasEntityHeader(typeName, guid, attributes); + } + + private void setField(String fieldName, Object value) throws Exception { + Field field = AtlasRelationshipStoreV2.class.getDeclaredField(fieldName); + + field.setAccessible(true); + field.set(relationshipStore, value); + } +} diff --git a/webapp/src/test/java/org/apache/atlas/web/rest/SearchDownloadFileValidatorTest.java b/webapp/src/test/java/org/apache/atlas/web/rest/SearchDownloadFileValidatorTest.java new file mode 100644 index 00000000000..5764665ffd3 --- /dev/null +++ b/webapp/src/test/java/org/apache/atlas/web/rest/SearchDownloadFileValidatorTest.java @@ -0,0 +1,136 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE + * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.apache.atlas.web.rest; + +import org.apache.atlas.common.TestUtility; +import org.apache.atlas.exception.AtlasBaseException; +import org.apache.commons.io.FileUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class SearchDownloadFileValidatorTest { + private static final String VALID_FILE_NAME = "admin_BASIC_2026-07-23_09-01-50.437.csv"; + + private File userDownloadDir; + + @BeforeMethod + public void setUp() throws IOException { + userDownloadDir = Files.createTempDirectory("search-download-validator-test").toFile(); + } + + @AfterMethod + public void tearDown() throws IOException { + FileUtils.deleteDirectory(userDownloadDir); + } + + @Test + public void testResolveDownloadFile_ValidFileName() throws Exception { + File expectedFile = new File(userDownloadDir, VALID_FILE_NAME); + + assertTrue(expectedFile.createNewFile()); + + File resolvedFile = SearchDownloadFileValidator.resolveDownloadFile(VALID_FILE_NAME, userDownloadDir); + + assertEquals(resolvedFile, expectedFile); + } + + @Test + public void testResolveDownloadFile_ValidDslFileName() throws Exception { + String fileName = "admin_DSL_2026-07-23_09-01-50.437.csv"; + File expectedFile = new File(userDownloadDir, fileName); + + assertTrue(expectedFile.createNewFile()); + + File resolvedFile = SearchDownloadFileValidator.resolveDownloadFile(fileName, userDownloadDir); + + assertEquals(resolvedFile, expectedFile); + } + + @Test + public void testResolveDownloadFile_PathTraversal_ThrowsException() { + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile("../../../../conf/users-credentials.properties", userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } + + @Test + public void testResolveDownloadFile_ParentDirectorySegment_ThrowsException() { + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile("..", userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } + + @Test + public void testResolveDownloadFile_AbsolutePath_ThrowsException() { + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile("/etc/passwd", userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } + + @Test + public void testResolveDownloadFile_InvalidExtension_ThrowsException() { + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile("admin_BASIC_2026-07-23_09-01-50.437.txt", userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } + + @Test + public void testResolveDownloadFile_InvalidPrefix_ThrowsException() { + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile("results.csv", userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } + + @Test + public void testResolveDownloadFile_BlankFileName_ThrowsException() { + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile(" ", userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } + + @Test + public void testResolveDownloadFile_SymbolicLinkOutsideUserDir_ThrowsException() throws Exception { + File outsideDir = Files.createTempDirectory("search-download-outside").toFile(); + File outsideFile = new File(outsideDir, VALID_FILE_NAME); + + try { + assertTrue(outsideFile.createNewFile()); + + File linkInUserDir = new File(userDownloadDir, VALID_FILE_NAME); + + Files.createSymbolicLink(linkInUserDir.toPath(), outsideFile.toPath()); + + AtlasBaseException exception = expectThrows(AtlasBaseException.class, + () -> SearchDownloadFileValidator.resolveDownloadFile(VALID_FILE_NAME, userDownloadDir)); + + TestUtility.assertBadRequests(exception, SearchDownloadFileValidator.INVALID_DOWNLOAD_FILE_NAME_MSG); + } finally { + FileUtils.deleteDirectory(outsideDir); + } + } +}