diff --git a/core/src/main/java/org/pentaho/di/connections/ConnectionManager.java b/core/src/main/java/org/pentaho/di/connections/ConnectionManager.java index 0c8bf1e2d0c3..320d14c5ffa6 100644 --- a/core/src/main/java/org/pentaho/di/connections/ConnectionManager.java +++ b/core/src/main/java/org/pentaho/di/connections/ConnectionManager.java @@ -22,10 +22,13 @@ package org.pentaho.di.connections; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import org.pentaho.di.connections.utils.EncryptUtils; -import org.pentaho.di.core.bowl.Bowl; +import org.pentaho.di.connections.utils.VFSConnectionTestOptions; +import org.pentaho.di.connections.vfs.VFSConnectionDetails; +import org.pentaho.di.connections.vfs.VFSConnectionProvider; import org.pentaho.di.core.exception.KettleException; -import org.pentaho.di.core.variables.VariableSpace; import org.pentaho.metastore.api.IMetaStore; import org.pentaho.metastore.api.exceptions.MetaStoreException; import org.pentaho.metastore.persist.MetaStoreFactory; @@ -39,6 +42,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.Objects; import static org.pentaho.metastore.util.PentahoDefaults.NAMESPACE; @@ -291,6 +295,27 @@ public boolean test( T connectionDetails ) throws return connectionProvider.test( connectionDetails ); } + /** + * Tests if a given VFS connection is valid, optionally, with certain testing options. + * + * @param connectionDetails The VFS connection. + * @param options The testing options, or {@code null}. When {@code null}, a default instance of + * {@link VFSConnectionTestOptions} is constructed and used. + * @return {@code true} if the connection is valid; {@code false}, otherwise. + */ + public boolean test( @NonNull T connectionDetails, @Nullable VFSConnectionTestOptions options ) + throws KettleException { + Objects.requireNonNull( connectionDetails ); + + if ( options == null ) { + options = new VFSConnectionTestOptions(); + } + + VFSConnectionProvider connectionProvider = (VFSConnectionProvider) connectionProviders.get(connectionDetails.getType()); + + return connectionProvider.test( connectionDetails, options ); + } + /** * Delete a connection by name from the default * diff --git a/core/src/main/java/org/pentaho/di/connections/utils/VFSConnectionTestOptions.java b/core/src/main/java/org/pentaho/di/connections/utils/VFSConnectionTestOptions.java new file mode 100644 index 000000000000..881dff374f8d --- /dev/null +++ b/core/src/main/java/org/pentaho/di/connections/utils/VFSConnectionTestOptions.java @@ -0,0 +1,57 @@ + +/*! ****************************************************************************** + * + * Pentaho Data Integration + * + * Copyright (C) 2024 by Hitachi Vantara : http://www.pentaho.com + * + ******************************************************************************* + * + * Licensed 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.pentaho.di.connections.utils; + +/** + * This class contains options that control the testing of VFS Connection. + */ +public class VFSConnectionTestOptions { + + private boolean ignoreRootPath; + + public VFSConnectionTestOptions() { + } + + public VFSConnectionTestOptions( boolean ignoreRootPath ) { + this.ignoreRootPath = ignoreRootPath; + } + + /** + * Indicates if the root path should be ignored when testing the connection. + * @return {@code true}, if the root path should be ignored; {@code false}, otherwise. + */ + public boolean isIgnoreRootPath() { + return ignoreRootPath; + } + + + /** + * Sets if the root path should be ignored when testing the connection. + * @param ignoreRootPath The ignore root path flag. + * {@code true} to ignore the root path; {@code false}, otherwise. + */ + public void setIgnoreRootPath( boolean ignoreRootPath ) { + this.ignoreRootPath = ignoreRootPath; + } +} diff --git a/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionDetails.java b/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionDetails.java index f8bdd39a991f..c434d2c64170 100644 --- a/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionDetails.java +++ b/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionDetails.java @@ -23,6 +23,7 @@ package org.pentaho.di.connections.vfs; import edu.umd.cs.findbugs.annotations.NonNull; +import org.apache.commons.lang.StringUtils; import org.pentaho.metastore.persist.MetaStoreAttribute; import java.util.ArrayList; @@ -39,6 +40,9 @@ public abstract class BaseVFSConnectionDetails implements VFSConnectionDetails { @MetaStoreAttribute private List baRoles = new ArrayList<>(); + @MetaStoreAttribute + private String rootPath; + @NonNull @Override public List getBaRoles() { @@ -52,6 +56,26 @@ public Map getProperties() { return props; } + /** + * Gets if the VFS connection supports root path or not. + * @returns {@code true} if VFS connection supports root path; {@code false} otherwise. + * @default {@code true} + */ + @Override + public boolean isSupportsRootPath() { + return true; + } + + @Override + public String getRootPath() { + return rootPath; + } + + @Override + public void setRootPath( String rootPath ) { + this.rootPath = StringUtils.isEmpty( rootPath ) ? null : rootPath; + } + /** * Adds base/default properties to properties of connection instance. *

diff --git a/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionProvider.java b/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionProvider.java index 8ddfb2bedcb7..9a579844f614 100644 --- a/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionProvider.java +++ b/core/src/main/java/org/pentaho/di/connections/vfs/BaseVFSConnectionProvider.java @@ -22,22 +22,35 @@ package org.pentaho.di.connections.vfs; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileType; import org.pentaho.di.connections.ConnectionDetails; import org.pentaho.di.connections.ConnectionManager; +import org.pentaho.di.connections.utils.VFSConnectionTestOptions; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.row.value.ValueMetaBase; import org.pentaho.di.core.util.Utils; import org.pentaho.di.core.variables.VariableSpace; import org.pentaho.di.core.variables.Variables; +import org.pentaho.di.core.vfs.KettleVFS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.Objects; import java.util.function.Supplier; +import static org.pentaho.di.connections.vfs.provider.ConnectionFileObject.DELIMITER; + public abstract class BaseVFSConnectionProvider implements VFSConnectionProvider { private Supplier connectionManagerSupplier = ConnectionManager::getInstance; + private static final Logger LOGGER = LoggerFactory.getLogger( BaseVFSConnectionProvider.class ); + @Override public List getNames() { return connectionManagerSupplier.get().getNamesByType( getClass() ); @@ -90,4 +103,74 @@ protected static boolean getBooleanValueOfVariable( VariableSpace space, String protected VariableSpace getSpace( ConnectionDetails connectionDetails ) { return connectionDetails.getSpace() == null ? Variables.getADefaultVariableSpace() : connectionDetails.getSpace(); } + + @Override + public boolean test( @NonNull T connectionDetails, @NonNull VFSConnectionTestOptions connectionTestOptions ) throws KettleException { + boolean valid = test( connectionDetails ); + if ( !valid ) { + return false; + } + + if ( !connectionDetails.isSupportsRootPath() || connectionTestOptions.isIgnoreRootPath() ) { + return true; + } + + String resolvedRootPath = getResolvedRootPath( connectionDetails ); + if ( StringUtils.isEmpty( resolvedRootPath ) ) { + return !connectionDetails.isRootPathRequired(); + } + + String internalUrl = buildUrl( connectionDetails, resolvedRootPath ); + FileObject fileObject = KettleVFS.getFileObject( internalUrl, new Variables(), getOpts( connectionDetails ) ); + + try { + return fileObject.exists() && this.isFolder( fileObject ); + } catch ( FileSystemException fileSystemException ) { + LOGGER.error( fileSystemException.getMessage() ); + return false; + } + } + + @Override + public String getResolvedRootPath( @NonNull T connectionDetails ) { + if ( StringUtils.isNotEmpty( connectionDetails.getRootPath() ) ) { + VariableSpace space = getSpace( connectionDetails ); + String resolvedRootPath = getVar( connectionDetails.getRootPath(), space ); + if ( StringUtils.isNotBlank( resolvedRootPath ) ) { + return normalizeRootPath( resolvedRootPath ); + } + } + + return StringUtils.EMPTY; + } + + private String normalizeRootPath( String rootPath ) { + if ( StringUtils.isNotEmpty( rootPath ) ) { + if ( !rootPath.startsWith( DELIMITER ) ) { + rootPath = DELIMITER + rootPath; + } + if (rootPath.endsWith( DELIMITER ) ) { + rootPath = rootPath.substring( 0, rootPath.length() - 1 ); + } + } + return rootPath; + } + + private String buildUrl( VFSConnectionDetails connectionDetails, String rootPath ) { + String domain = connectionDetails.getDomain(); + if ( !domain.isEmpty() ) { + domain = DELIMITER + domain; + } + return connectionDetails.getType() + ":/" + domain + rootPath; + } + + private boolean isFolder( @NonNull FileObject fileObject ) { + try { + + return fileObject.getType() != null && fileObject.getType().equals( FileType.FOLDER ); + } catch ( FileSystemException e ) { + return false; + } + } } + diff --git a/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionDetails.java b/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionDetails.java index 520f8328546f..d909cb2a4bae 100644 --- a/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionDetails.java +++ b/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionDetails.java @@ -57,20 +57,83 @@ default boolean isRootPathRequired() { } /** + * Gets the root folder path of this VFS connection. + *

+ * The root folder path allows limiting the files exposed through a pvfs URL. + *

+ * The default interface implementation exists to ensure backward compatibility and returns {@code null}. + *

+ * Semantics of the Root Folder Path + *

+ * Assume a connection without a configured root folder path, connection-name. + * The general structure of a pvfs URL that resolves to a file in this connection is + * pvfs://(connection-name)/(rest-path). + * If the rest-path component is split in two parts, the root path and the remainder, + * the following form is achieved: pvfs://(connection-name)/(root-path)/(rest-rest-path). + *

+ * Assume a connection configured with the root folder path root-path, all other configurations equal, + * named connection-with-root-path. + * The same file would be exposed by a pvfs URL in which the root-path component is omitted: + * pvfs://(connection-with-root-path)/(rest-rest-path). + *

+ * Necessarily, the configured root path must identify a file of type folder. + *

+ * Files which are not descendant of a connection's root folder path cannot be identified/accessed using a + * pvfs URL. Folder segments of a pvfs URL cannot have the special names . or + * ... + *

+ * Syntax of the Root Folder Path + *

+ * The syntax of the root folder path is that of one or more folder names separated by a folder separator, + * /. For example, the following would be syntactically valid: my-vfs-bucket/my-folder. + * While a leading or a trailing folder separator should be tolerated, a normalized root folder path + * should have none. + *

+ * The value stored in this property is subject to variable substitution and thus may not conform to the syntax + * of a root folder path. The syntax is validated only after variable substitution is performed. + *

+ * Impact of Root Folder Path on Provider URLs + *

+ * While omitted from the pvfs URL, the root folder path is incorporated in the provider-specific + * (a.k.a. internal) URL, as a result of the conversion process from pvfs to provider URL. + * The root folder path is not a required component of provider URLs, and files which are not descendants of the root + * folder path are still resolvable. The root folder path is not a security feature, by itself. + *

+ * The general structure of a provider URL corresponding to the above pvfs URL is like: + * (scheme):// [(domain) /] [(root-path) /] [(rest-rest-path)] + *

+ * Where the scheme component is given by the {@link #getType()} property, and the domain component is + * given by the {@link #getDomain()} property. + *

+ * The provider URL structure for specific providers may vary from this general structure. However, the semantics of + * the root folder path property should be respected. + *

+ * Examples of pvfs and Provider URLs + *

+ * Given an S3 connection, with a configured root folder path of my-bucket/my-folder, + * the pvfs URL, pvfs://my-s3-connection/my-sub-folder/my-file, would convert to the + * provider URL, s3://my-bucket/my-folder/my-sub-folder/my-file. + *

+ * Given an HCP connection, with a configured root folder path of my-folder, and a configured domain of + * my-domain.com:3000,the pvfs URL, + * pvfs://my-hcp-connection/my-sub-folder/my-file, would convert to the provider URL, + * hcp://my-domain.com:3000/my-folder/my-sub-folder/my-file. * - * Gets the root path of a vfs connection. - * - * Defaults to {@code null} - * return the root path. + * @return A non-empty root path, if any; {@code null}, otherwise. */ default String getRootPath() { return null; } /** - * Sets the root path, given as a string + * Sets the root folder path, given as a string. + *

+ * An empty root folder path value should be converted to {@code null}. + * Further syntax validation is performed only after variable substitution. + *

+ * The default interface implementation exists to ensure backward compatibility and does nothing. * - * @param rootPath The root path + * @param rootPath The root path. */ default void setRootPath( String rootPath ) { } diff --git a/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionProvider.java b/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionProvider.java index 6dacf3f72178..2dde93a35dd9 100644 --- a/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionProvider.java +++ b/core/src/main/java/org/pentaho/di/connections/vfs/VFSConnectionProvider.java @@ -2,7 +2,7 @@ * * Pentaho Data Integration * - * Copyright (C) 2019-2023 by Hitachi Vantara : http://www.pentaho.com + * Copyright (C) 2019-2024 by Hitachi Vantara : http://www.pentaho.com * ******************************************************************************* * @@ -22,13 +22,14 @@ package org.pentaho.di.connections.vfs; +import edu.umd.cs.findbugs.annotations.NonNull; import org.apache.commons.vfs2.FileObject; import org.apache.commons.vfs2.FileSystemOptions; -import org.pentaho.di.connections.ConnectionDetails; import org.pentaho.di.connections.ConnectionProvider; +import org.pentaho.di.connections.utils.VFSConnectionTestOptions; +import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleFileException; import org.pentaho.di.core.variables.Variables; -import org.pentaho.di.core.variables.VariableSpace; import org.pentaho.di.core.vfs.KettleVFS; import java.util.List; @@ -57,4 +58,49 @@ default FileObject getDirectFile( T connectionDetails, String path ) throws Kett return KettleVFS.getFileObject( pvfsUrl, new Variables(), getOpts( connectionDetails ) ); } + /** + * Tests if a given VFS connection is valid, optionally, with certain testing options. + *

+ * This method should first delegate to {@link ConnectionProvider#test(ConnectionDetails)} to perform basic + * validation, independent of the connection's root path, {@link VFSConnectionDetails#getRootPath}, if any, + * immediately returning {@code false}, when unsuccessful. + *

+ * When base validation is successful, if {@code options} has a {@code true} + * {@link VFSConnectionTestOptions#isIgnoreRootPath()}, this method should immediately return {@code true}. + *

+ * Otherwise, the method should validate that the connection's root folder path is valid, taking into account the + * values of {@link VFSConnectionDetails#isSupportsRootPath()}, {@link VFSConnectionDetails#isRootPathRequired()} and + * {@link VFSConnectionDetails#getRootPath()}. + *

+ * The default implementation exists for backward compatibility reasons and simply delegates to + * {@link ConnectionProvider#test(ConnectionDetails)}. + * @param connectionDetails The VFS connection. + * @param options The testing options, or {@code null}. When {@code null}, a default instance of + * {@link VFSConnectionTestOptions} is constructed and used. + * @return {@code true} if the provided rootPath is valid; {@code false} otherwise. + */ + default boolean test( @NonNull T connectionDetails, @NonNull VFSConnectionTestOptions options ) throws KettleException { + return test( connectionDetails ); + } + + /** + * Gets the resolved root path of a given connection. + * + * @param connectionDetails The VFS connection. + * @return The non-empty resolved root path, if any; {@code null}, if. none. + */ + default String getResolvedRootPath( @NonNull T connectionDetails ) { + return connectionDetails.getRootPath(); + } + + /** + * Checks if a given connection uses buckets in its current configuration. + *

+ * A connection is using buckets if it can have buckets, as determined by {@link ConnectionDetails#hasBuckets()}, and if its {@link #getResolvedRootPath() resolved root path} is empty. + * @param connectionDetails The VFS connection. + * @return {@code true} if a connection has buckets; {@code false} otherwise. + */ + default boolean usesBuckets( @NonNull T connectionDetails ) { + return connectionDetails.hasBuckets() && getResolvedRootPath( connectionDetails ) == null; + } } diff --git a/core/src/test/java/org/pentaho/di/connections/ConnectionManagerTest.java b/core/src/test/java/org/pentaho/di/connections/ConnectionManagerTest.java index 0646107cb239..3b5b9e33787d 100644 --- a/core/src/test/java/org/pentaho/di/connections/ConnectionManagerTest.java +++ b/core/src/test/java/org/pentaho/di/connections/ConnectionManagerTest.java @@ -28,17 +28,21 @@ import org.junit.Test; import org.pentaho.di.connections.common.bucket.TestConnectionDetails; import org.pentaho.di.connections.common.bucket.TestConnectionProvider; -import org.pentaho.di.connections.vfs.VFSConnectionDetails; +import org.pentaho.di.connections.utils.VFSConnectionTestOptions; import org.pentaho.di.connections.vfs.VFSHelper; import org.pentaho.di.connections.vfs.VFSLookupFilter; -import org.pentaho.di.core.bowl.DefaultBowl; +import org.pentaho.di.connections.vfs.VFSConnectionProvider; +import org.pentaho.di.connections.vfs.VFSConnectionDetails; import org.pentaho.di.core.KettleClientEnvironment; +import org.pentaho.di.core.bowl.DefaultBowl; +import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.variables.VariableSpace; import org.pentaho.metastore.persist.MetaStoreFactory; import org.pentaho.metastore.stores.memory.MemoryMetaStore; import java.util.List; +import static org.mockito.Mockito.*; import static org.pentaho.metastore.util.PentahoDefaults.NAMESPACE; /** @@ -255,6 +259,31 @@ public void testDefaultPropertiesNotNull() { Assert.assertNotNull( connectionDetails.getProperties() ); Assert.assertNotNull( connectionDetails.getProperties().get( "baRoles" ) ); } + @Test + public void testConnection() throws KettleException { + + VFSConnectionTestOptions vfsConnectionTestOptions = new VFSConnectionTestOptions( true ); + VFSConnectionProvider vfsConnectionProvider = mock( VFSConnectionProvider.class ); + VFSConnectionDetails vfsConnectionDetails = mock( VFSConnectionDetails.class ); + + connectionManager.addConnectionProvider( "test", vfsConnectionProvider ); + when( vfsConnectionDetails.getType() ).thenReturn( "test" ); + + connectionManager.test( vfsConnectionDetails, vfsConnectionTestOptions ); + verify( vfsConnectionProvider, times(1)).test( vfsConnectionDetails, vfsConnectionTestOptions ); + } + + @Test + public void testConnectionWithEmptyVFSTestOptions() throws KettleException { + VFSConnectionProvider vfsConnectionProvider = mock( VFSConnectionProvider.class ); + VFSConnectionDetails vfsConnectionDetails = mock( VFSConnectionDetails.class ); + + connectionManager.addConnectionProvider( "test", vfsConnectionProvider ); + when( vfsConnectionDetails.getType() ).thenReturn( "test" ); + when( vfsConnectionProvider.test( vfsConnectionDetails ) ).thenReturn( true ); + connectionManager.test( vfsConnectionDetails, null ); + verify( vfsConnectionProvider, times(1) ).test( eq( vfsConnectionDetails ), any( VFSConnectionTestOptions.class ) ); + } private void addProvider() { TestConnectionProvider testConnectionProvider = new TestConnectionProvider( connectionManager ); @@ -297,9 +326,5 @@ public static class BadConnectionDetails implements ConnectionDetails { @Override public void setSpace( VariableSpace space ) { } - - } - - } diff --git a/core/src/test/java/org/pentaho/di/connections/vfs/provider/BaseVFSConnectionProviderTest.java b/core/src/test/java/org/pentaho/di/connections/vfs/provider/BaseVFSConnectionProviderTest.java new file mode 100644 index 000000000000..d6109376e7d2 --- /dev/null +++ b/core/src/test/java/org/pentaho/di/connections/vfs/provider/BaseVFSConnectionProviderTest.java @@ -0,0 +1,85 @@ + +package org.pentaho.di.connections.vfs.provider; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.pentaho.di.connections.utils.VFSConnectionTestOptions; +import org.pentaho.di.connections.vfs.BaseVFSConnectionDetails; +import org.pentaho.di.connections.vfs.BaseVFSConnectionProvider; +import org.pentaho.di.connections.vfs.VFSConnectionDetails; +import org.pentaho.di.core.exception.KettleException; + +import static org.mockito.Mockito.when; + +public class BaseVFSConnectionProviderTest { + @Mock + BaseVFSConnectionDetails baseVFSConnectionDetails; + + @Before + public void setup() { + baseVFSConnectionDetails = Mockito.spy( BaseVFSConnectionDetails.class ); + } + + @Test + public void testWithProvidedRootPath() throws KettleException { + + BaseVFSConnectionProvider baseVFSConnectionProvider = Mockito.mock( BaseVFSConnectionProvider.class, Answers.CALLS_REAL_METHODS ); + + when( baseVFSConnectionProvider.test( baseVFSConnectionDetails ) ).thenReturn( true ); + when( baseVFSConnectionDetails.getRootPath() ).thenReturn( "xyz/" ); + Assert.assertFalse( baseVFSConnectionProvider.test( baseVFSConnectionDetails, getVFSTestOptionWhenNotIgnoringRootPath() ) ); + } + + @Test + public void testWhenConnectionTestIsInvalid() throws KettleException { + + BaseVFSConnectionProvider baseVFSConnectionProvider = Mockito.mock( BaseVFSConnectionProvider.class, Answers.CALLS_REAL_METHODS ); + Assert.assertFalse( baseVFSConnectionProvider.test( baseVFSConnectionDetails, getVFSTestOptionWhenNotIgnoringRootPath() ) ); + } + + + @Test + public void testWhenConnectionDontSupportRootPath() throws KettleException { + + BaseVFSConnectionProvider baseVFSConnectionProvider = Mockito.mock( BaseVFSConnectionProvider.class, Answers.CALLS_REAL_METHODS ); + + when( baseVFSConnectionProvider.test( baseVFSConnectionDetails ) ).thenReturn( true ); + when( baseVFSConnectionDetails.isSupportsRootPath() ).thenReturn( false ); + Assert.assertTrue( baseVFSConnectionProvider.test( baseVFSConnectionDetails, getVFSTestOptionWhenIgnoringRootPath() ) ); + } + + @Test + public void testWithIgnoringRootPath() throws KettleException { + + BaseVFSConnectionProvider baseVFSConnectionProvider = Mockito.mock( BaseVFSConnectionProvider.class, Answers.CALLS_REAL_METHODS ); + + when( baseVFSConnectionProvider.test( baseVFSConnectionDetails ) ).thenReturn( true ); + Assert.assertTrue( baseVFSConnectionProvider.test( baseVFSConnectionDetails, getVFSTestOptionWhenNotIgnoringRootPath() ) ); + } + + + @Test + public void testWithConnectionDomain() throws KettleException { + + BaseVFSConnectionProvider baseVFSConnectionProvider = Mockito.mock( BaseVFSConnectionProvider.class, Answers.CALLS_REAL_METHODS ); + + when( baseVFSConnectionProvider.test( baseVFSConnectionDetails ) ).thenReturn( true ); + when( baseVFSConnectionDetails.getRootPath() ).thenReturn( "xyz/" ); + when( baseVFSConnectionDetails.getDomain() ).thenReturn( "local" ); + Assert.assertFalse( baseVFSConnectionProvider.test( baseVFSConnectionDetails, getVFSTestOptionWhenNotIgnoringRootPath() ) ); + } + + private VFSConnectionTestOptions getVFSTestOptionWhenNotIgnoringRootPath() { + return new VFSConnectionTestOptions( false ); + } + + private VFSConnectionTestOptions getVFSTestOptionWhenIgnoringRootPath() { + VFSConnectionTestOptions vfsConnectionTestOptions = new VFSConnectionTestOptions(); + vfsConnectionTestOptions.setIgnoreRootPath( true ); + return vfsConnectionTestOptions; + } +}