diff --git a/build-android.gradle b/build-android.gradle deleted file mode 100644 index f84bf34e..00000000 --- a/build-android.gradle +++ /dev/null @@ -1,67 +0,0 @@ -buildscript { - repositories { - jcenter() - google() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.1.0' - } -} - -apply plugin: 'com.android.library' - -repositories { - jcenter() - google() -} - -group = 'com.hierynomus' - -dependencies { - implementation SLF4J_API - implementation BCPROV_JDK15ON - implementation MBASSADOR - implementation ASN_ONE - testRuntimeOnly CGLIB_NODEP - testRuntimeOnly OBJENESIS - testImplementation SPOCK_CORE - testImplementation COMMONS_IO - testRuntimeOnly LOGBACK_CLASSIC -} - -def androidSdkVersion = 19 - -android { - compileSdkVersion androidSdkVersion - - defaultConfig { - minSdkVersion androidSdkVersion - targetSdkVersion androidSdkVersion - - manifestPlaceholders = [ - minSdkVersion : androidSdkVersion, - targetSdkVersion: androidSdkVersion - ] - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - sourceSets { - main.java { - exclude( - 'com/hierynomus/smbj/auth/SpnegoAuthenticator.java', - 'com/hierynomus/smbj/auth/ExtendedGSSContext.java', - 'com/hierynomus/smbj/auth/GSSAuthenticationContext.java', - 'com/hierynomus/smbj/transport/tcp/async/**/*.java' - ) - } - } - - lintOptions { - abortOnError false - } -} diff --git a/build.gradle b/build.gradle index 39a79e4f..610e9232 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,6 @@ plugins { id "signing" id 'pl.allegro.tech.build.axion-release' version '1.15.3' id "com.github.hierynomus.license" version "0.16.1" - id "com.bmuschko.docker-remote-api" version "9.2.1" // id 'ru.vyarus.java-lib' version '2.1.0' id 'ru.vyarus.github-info' version '1.5.0' id "io.github.gradle-nexus.publish-plugin" version "1.3.0" @@ -98,6 +97,7 @@ testing { implementation SPOCK_CORE implementation COMMONS_IO implementation "org.mockito:mockito-core:5.4.0" + implementation "org.assertj:assertj-core:3.24.2" runtimeOnly LOGBACK_CLASSIC } @@ -122,6 +122,14 @@ testing { } integrationTest(JvmTestSuite) { + dependencies { + implementation project() + implementation "ch.qos.logback:logback-classic:1.3.8" + implementation 'org.testcontainers:testcontainers:1.19.0' + implementation 'org.testcontainers:junit-jupiter:1.19.0' + implementation 'org.apache.commons:commons-compress:1.24.0' + } + sources { java { srcDirs = ['src/it/java'] @@ -222,30 +230,6 @@ jacocoTestReport { } } -task buildItestImage(type: DockerBuildImage) { - inputDir = file('src/it/docker-image') - images.add('smbj/smbj-itest:latest') -} - -task createItestContainer(type: DockerCreateContainer) { - dependsOn buildItestImage - targetImageId buildItestImage.getImageId() - hostConfig.portBindings = ['445:445'] - hostConfig.autoRemove = true -} - -task startItestContainer(type: DockerStartContainer) { - dependsOn createItestContainer - targetContainerId createItestContainer.getContainerId() -} - -task stopItestContainer(type: DockerStopContainer) { - targetContainerId createItestContainer.getContainerId() -} - -project.tasks.integrationTest.dependsOn(startItestContainer) -project.tasks.integrationTest.finalizedBy(stopItestContainer) - project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build]) project.tasks.jacocoTestReport.dependsOn(project.tasks.test) diff --git a/gradle.properties b/gradle.properties index db0d9616..893be498 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,6 @@ COMMONS_IO=commons-io:commons-io:2.13.0 LOGBACK_CLASSIC=ch.qos.logback:logback-classic:1.3.8 MBASSADOR=net.engio:mbassador:1.3.0 OBJENESIS=org.objenesis:objenesis:3.3 -SLF4J_API=org.slf4j:slf4j-api:2.0.7 +SLF4J_API=org.slf4j:slf4j-api:2.0.9 SPOCK_CORE=org.spockframework:spock-core:2.3-groovy-3.0 ASN_ONE=com.hierynomus:asn-one:0.6.0 diff --git a/src/it/groovy/com/hierynomus/smbj/DfsIntegrationSpec.groovy b/src/it/groovy/com/hierynomus/smbj/DfsIntegrationSpec.groovy deleted file mode 100644 index 2c3c0f1b..00000000 --- a/src/it/groovy/com/hierynomus/smbj/DfsIntegrationSpec.groovy +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C)2016 - SMBJ Contributors - * - * 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 com.hierynomus.smbj - -import com.hierynomus.msdtyp.AccessMask -import com.hierynomus.mssmb2.SMB2CreateDisposition -import com.hierynomus.mssmb2.SMB2Dialect -import com.hierynomus.mssmb2.SMB2ShareAccess -import com.hierynomus.security.bc.BCSecurityProvider -import com.hierynomus.smbj.auth.AuthenticationContext -import com.hierynomus.smbj.connection.Connection -import com.hierynomus.smbj.session.Session -import com.hierynomus.smbj.share.DiskShare -import spock.lang.Ignore -import spock.lang.Specification - -class DfsIntegrationSpec extends Specification { - Session session - Connection connection - SMBClient client - - def setup() { - def config = SmbConfig - .builder() - .withDialects(SMB2Dialect.SMB_2_1) - .withMultiProtocolNegotiate(true) - .withSigningRequired(true) - .withDfsEnabled(true) - .build() - client = new SMBClient(config) - connection = client.connect("127.0.0.1") - session = connection.authenticate(new AuthenticationContext("smbj", "smbj".toCharArray(), null)) - } - - def cleanup() { - session.logoff() - connection.close() - } - - def "should connect to DFS share"() { - given: - def share = session.connectShare("dfs") - - when: - def list = (share as DiskShare).list("") - - then: - list.fileName.contains("public") - list.fileName.contains("user") - - cleanup: - share.close() - } - - def "should list contents of DFS virtual directory"() { - given: - def share = session.connectShare("dfs") - - when: - def dir = (share as DiskShare).openDirectory("user", EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null) - - then: - dir.list().fileName.contains(".") - - cleanup: - share.close() - } - - def "should connect to fallback if first link is broken"() { - given: - def share = session.connectShare("dfs") - - when: - def dir = (share as DiskShare).openDirectory("firstfail-public", EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null) - - then: - dir.list().fileName.contains(".") - - cleanup: - share.close() - } - - def "should have filename for regular directory share when dfs is enabled GH#603"() { - given: - def userShare = session.connectShare("user") - userShare.mkdir("a_directory") - - when: - def dir = (userShare as DiskShare).openDirectory("a_directory", EnumSet.of(AccessMask.GENERIC_READ), null, EnumSet.of(SMB2ShareAccess.FILE_SHARE_READ), SMB2CreateDisposition.FILE_OPEN, null) - - then: - dir.getPath() == "a_directory" - dir.getFileName() == "\\\\127.0.0.1\\user\\a_directory" - dir.getUncPath() == "\\\\127.0.0.1\\user\\a_directory" - - cleanup: - dir.close() - userShare.rmdir("a_directory", false) - userShare.close() - } - -} diff --git a/src/it/groovy/com/hierynomus/smbj/IntegrationTest.groovy b/src/it/groovy/com/hierynomus/smbj/IntegrationTest.groovy deleted file mode 100644 index cc608892..00000000 --- a/src/it/groovy/com/hierynomus/smbj/IntegrationTest.groovy +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C)2016 - SMBJ Contributors - * - * 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 com.hierynomus.smbj - -import com.hierynomus.msdtyp.AccessMask -import com.hierynomus.msfscc.fileinformation.FileIdFullDirectoryInformation -import com.hierynomus.msfscc.fileinformation.FileInternalInformation -import com.hierynomus.mssmb2.SMB2Dialect -import com.hierynomus.mssmb2.SMB2ShareAccess -import com.hierynomus.security.bc.BCSecurityProvider -import com.hierynomus.smbj.auth.AuthenticationContext -import com.hierynomus.smbj.connection.Connection -import com.hierynomus.smbj.share.Directory -import com.hierynomus.smbj.share.DiskShare -import com.hierynomus.smbj.share.File -import spock.lang.Specification - -import static com.hierynomus.mssmb2.SMB2CreateDisposition.FILE_OPEN - -class IntegrationTest extends Specification { - static final def IP = "127.0.0.1" - static final def AUTH = new AuthenticationContext("smbj", "smbj".toCharArray(), null) - static final def SHARE = "public" - static final def FOLDER_THAT_EXISTS = "folder" - static final def FILE_THAT_EXISTS = "test.txt" - static final def FOLDER_THAT_DOES_NOT_EXIST = "foo" - - - def config = SmbConfig.builder().withDialects(SMB2Dialect.SMB_3_0).withEncryptData(true).withSigningRequired(true).withMultiProtocolNegotiate(true).withDfsEnabled(true).withSecurityProvider(new BCSecurityProvider()) - def client = _ - Connection connection = null - - def setup() { - client = new SMBClient(config.build()) - connection = client.connect(IP) - } - - def cleanup() { - connection.close() - } - - def "should be connected"() { - expect: - connection.connected - } - - def "should authenticate"() { - when: - def session = connection.authenticate(AUTH) - - then: - session.sessionId != null - } - - def "should connect to share"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) - - then: - share instanceof DiskShare - share.treeConnect.treeId != null - share.isConnected() - } - - def "should check directory existence"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) as DiskShare - - then: - share.folderExists(FOLDER_THAT_EXISTS) - !share.folderExists(FOLDER_THAT_DOES_NOT_EXIST) - } - - def "should be able to list directories"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) as DiskShare - def children = share.list(FOLDER_THAT_EXISTS) - - then: - children.size() > 0 - } - - def "should be able to open directories"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) as DiskShare - def dir = share.open(FOLDER_THAT_EXISTS, EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - - then: - dir instanceof Directory - } - - def "should be able to open files"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) as DiskShare - def dir = share.open(FILE_THAT_EXISTS, EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - - then: - dir instanceof File - } - - def "should not fail closing connection twice"() { - given: - connection.close() - - when: - connection.close() - - then: - noExceptionThrown() - } - - def "should be able to read fileID from FileIdBothDirectoryInformation"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) as DiskShare - def children = share.list(FOLDER_THAT_EXISTS) - def firstChild = children.get(1); - def firstChildFile = share.open(FOLDER_THAT_EXISTS + '\\' + firstChild.getFileName(), EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - def fileInternalInformation = firstChildFile.getFileInformation(FileInternalInformation.class) - - then: - firstChild.fileId == fileInternalInformation.indexNumber // both refer to the same file ID - } - - def "should be able to read fileID from FileIdFullDirectoryInformation"() { - when: - def session = connection.authenticate(AUTH) - def share = session.connectShare(SHARE) as DiskShare - def children = share.list(FOLDER_THAT_EXISTS, FileIdFullDirectoryInformation.class) - def firstChild = children.get(1); - def firstChildFile = share.open(FOLDER_THAT_EXISTS + '\\' + firstChild.getFileName(), EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - def fileInternalInformation = firstChildFile.getFileInformation(FileInternalInformation.class) - - then: - firstChild.fileId == fileInternalInformation.indexNumber // both refer to the same file ID - } - - -} diff --git a/src/it/groovy/com/hierynomus/smbj/SMB2DirectoryIntegrationTest.groovy b/src/it/groovy/com/hierynomus/smbj/SMB2DirectoryIntegrationTest.groovy deleted file mode 100644 index 4a4f3b71..00000000 --- a/src/it/groovy/com/hierynomus/smbj/SMB2DirectoryIntegrationTest.groovy +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C)2016 - SMBJ Contributors - * - * 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 com.hierynomus.smbj - -import com.hierynomus.msdtyp.AccessMask -import com.hierynomus.mserref.NtStatus -import com.hierynomus.msfscc.FileAttributes -import com.hierynomus.msfscc.FileNotifyAction -import com.hierynomus.msfscc.fileinformation.FileStandardInformation -import com.hierynomus.mssmb2.SMB2ChangeNotifyFlags -import com.hierynomus.mssmb2.SMB2CompletionFilter -import com.hierynomus.mssmb2.SMB2CreateDisposition -import com.hierynomus.mssmb2.SMB2Dialect -import com.hierynomus.mssmb2.SMB2ShareAccess -import com.hierynomus.mssmb2.SMBApiException -import com.hierynomus.mssmb2.messages.SMB2Cancel -import com.hierynomus.mssmb2.messages.SMB2ChangeNotifyResponse -import com.hierynomus.protocol.commons.concurrent.Futures -import com.hierynomus.security.bc.BCSecurityProvider -import com.hierynomus.smbj.auth.AuthenticationContext -import com.hierynomus.smbj.common.SMBRuntimeException -import com.hierynomus.smbj.connection.Connection -import com.hierynomus.smbj.io.ArrayByteChunkProvider -import com.hierynomus.smbj.session.Session -import com.hierynomus.smbj.share.DiskShare -import com.hierynomus.smbj.transport.tcp.async.AsyncDirectTcpTransportFactory -import spock.lang.Specification -import spock.lang.Unroll - -import java.nio.charset.StandardCharsets - -import static com.hierynomus.mssmb2.SMB2CreateDisposition.* - -class SMB2DirectoryIntegrationTest extends Specification { - - DiskShare share - Session session - Connection connection - SMBClient client - - def setup() { - def config = SmbConfig - .builder() - .withDialects(SMB2Dialect.SMB_3_0) - .withEncryptData(true) - .withSecurityProvider(new BCSecurityProvider()) - .withMultiProtocolNegotiate(true) - .withTransportLayerFactory(new AsyncDirectTcpTransportFactory<>()) - .withSigningRequired(true).build() - client = new SMBClient(config) - connection = client.connect("127.0.0.1") - session = connection.authenticate(new AuthenticationContext("smbj", "smbj".toCharArray(), null)) - share = session.connectShare("user") as DiskShare - } - - def cleanup() { - connection.close() - } - - def "should correctly detect folder existence"() { - given: - share.mkdir("im_a_directory") - def src = share.openFile("im_a_file", EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_OVERWRITE_IF, null) - src.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)) - src.close() - - expect: - share.folderExists("im_a_directory") - !share.folderExists("im_a_file") - !share.folderExists("i_do_not_exist") - - cleanup: - share.rm("im_a_file") - share.rmdir("im_a_directory", false) - } - - @Unroll - def "should not fail if #method response is DELETE_PENDING for directory"() { - given: - def dir = share.openDirectory("to_be_removed", EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_CREATE, null) - dir.close() - dir = share.openDirectory("to_be_removed", EnumSet.of(AccessMask.GENERIC_ALL), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - dir.deleteOnClose() - - when: - func(share) - - then: - noExceptionThrown() - - where: - method | func - "rmdir" | { s -> s.rmdir("to_be_removed", false) } - "folderExists" | { s -> s.folderExists("to_be_removed") } - } - - def "should cancel ChangeNotify request"() { - given: - share.mkdir("to_be_watched") - def dir = share.openDirectory("to_be_watched", EnumSet.of(AccessMask.GENERIC_ALL), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - dir.deleteOnClose() - - def watch = dir.watchAsync(EnumSet.of(SMB2CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME), true) - - when: - watch.cancel(true) - def cancel = Futures.get(watch, SMBRuntimeException.Wrapper) - - then: - noExceptionThrown() - cancel.fileNotifyInfoList.size() == 0 - } - - def "should watch changes"() { - given: - share.mkdir("directory") - def directory = share.openDirectory("directory", - EnumSet.of(AccessMask.GENERIC_READ), - EnumSet.of(FileAttributes.FILE_ATTRIBUTE_DIRECTORY), - SMB2ShareAccess.ALL, - SMB2CreateDisposition.FILE_OPEN, - null) - - when: - def notifyResponseFuture = directory.watchAsync(EnumSet.of(SMB2CompletionFilter.FILE_NOTIFY_CHANGE_SIZE, SMB2CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME, SMB2CompletionFilter.FILE_NOTIFY_CHANGE_LAST_WRITE), false) - def file = share.openFile("directory/TestNotify.txt", - EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE, AccessMask.DELETE), - EnumSet.of(FileAttributes.FILE_ATTRIBUTE_NORMAL), - SMB2ShareAccess.ALL, - SMB2CreateDisposition.FILE_CREATE, - null) - file.write("Testing 123 123".getBytes(StandardCharsets.UTF_8), 0) - file.close() - - then: - def notifyResponse = notifyResponseFuture.get() - notifyResponse.fileNotifyInfoList != null - notifyResponse.fileNotifyInfoList.size() == 1 - def fileNotifyInfo01 = notifyResponse.fileNotifyInfoList.get(0) - fileNotifyInfo01.fileName == "TestNotify.txt" - fileNotifyInfo01.action == FileNotifyAction.FILE_ACTION_ADDED - directory.close() - - cleanup: - share.rmdir("directory", true) - } -} diff --git a/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy b/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy deleted file mode 100644 index 5f455d55..00000000 --- a/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy +++ /dev/null @@ -1,506 +0,0 @@ -/* - * Copyright (C)2016 - SMBJ Contributors - * - * 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 com.hierynomus.smbj - -import com.hierynomus.msdtyp.AccessMask -import com.hierynomus.mserref.NtStatus -import com.hierynomus.msfscc.FileNotifyAction -import com.hierynomus.msfscc.fileinformation.FileStandardInformation -import com.hierynomus.mssmb2.SMB2ChangeNotifyFlags -import com.hierynomus.mssmb2.SMB2CompletionFilter -import com.hierynomus.mssmb2.SMB2CreateDisposition -import com.hierynomus.mssmb2.SMB2Dialect -import com.hierynomus.mssmb2.SMB2ShareAccess -import com.hierynomus.mssmb2.SMBApiException -import com.hierynomus.security.bc.BCSecurityProvider -import com.hierynomus.mssmb2.SMB2LockFlag -import com.hierynomus.mssmb2.SMB2ShareAccess -import com.hierynomus.mssmb2.SMBApiException -import com.hierynomus.mssmb2.messages.submodule.SMB2LockElement -import com.hierynomus.smb.SMBPacket -import com.hierynomus.mssmb2.messages.SMB2Cancel -import com.hierynomus.mssmb2.messages.SMB2ChangeNotifyResponse -import com.hierynomus.protocol.commons.concurrent.Futures -import com.hierynomus.smbj.auth.AuthenticationContext -import com.hierynomus.smbj.common.SMBRuntimeException -import com.hierynomus.smbj.connection.Connection -import com.hierynomus.smbj.io.ArrayByteChunkProvider -import com.hierynomus.smbj.io.InputStreamByteChunkProvider -import com.hierynomus.smbj.session.Session -import com.hierynomus.smbj.share.DiskShare -import com.hierynomus.smbj.transport.tcp.async.AsyncDirectTcpTransportFactory -import com.hierynomus.smbj.transport.tcp.direct.DirectTcpTransportFactory -import spock.lang.Specification -import spock.lang.Unroll - -import java.nio.charset.StandardCharsets -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream - -import static com.hierynomus.mssmb2.SMB2CreateDisposition.* - -class SMB2FileIntegrationTest extends Specification { - - DiskShare share - Session session - Connection connection - SMBClient client - - def setup() { - def config = SmbConfig - .builder() - .withMultiProtocolNegotiate(true) - .withDialects(SMB2Dialect.SMB_3_0).withEncryptData(true) - .withTransportLayerFactory(new DirectTcpTransportFactory<>()) - .withSecurityProvider(new BCSecurityProvider()) - .withSigningRequired(true) - /*.withEncryptData(true)*/.build() - client = new SMBClient(config) - connection = client.connect("127.0.0.1") - session = connection.authenticate(new AuthenticationContext("smbj", "smbj".toCharArray(), null)) - share = session.connectShare("user") as DiskShare - } - - def cleanup() { - connection.close() - } - - def "should list contents of empty share"() { - when: - def list = share.list("") - - then: - list.size() == 2 - list.get(0).fileName == "." - list.get(1).fileName == ".." - } - - @Unroll - def "should create file and list contents of share"() { - given: - def f = share.openFile("test", EnumSet.of(AccessMask.GENERIC_ALL), null, SMB2ShareAccess.ALL, FILE_CREATE, null) - f.close() - - expect: - share.list(path).collect { it.fileName } contains "test" - - cleanup: - share.rm("test") - - where: - path << ["", null] - } - - def "should create directory and list contents"() { - given: - share.mkdir("folder-1") - - expect: - share.list("").collect { it.fileName } contains "folder-1" - share.list("folder-1").collect { it.fileName } == [".", ".."] - - cleanup: - share.rmdir("folder-1", true) - } - - def "should read file contents of file in directory"() { - given: - share.mkdir("api") - def textFile = share.openFile("api\\test.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_CREATE, null) - textFile.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)) - textFile.close() - - when: - def read = share.openFile("api\\test.txt", EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - - then: - def is = read.getInputStream() - is.readLines() == ["Hello World!"] - - cleanup: - is?.close() - read.close() - share.rmdir("api", true) - } - - def "should delete locked file"() { - given: - def lockedFile = share.openFile("locked", EnumSet.of(AccessMask.GENERIC_WRITE), null, EnumSet.noneOf(SMB2ShareAccess.class), FILE_CREATE, null) - - when: - share.rm("locked") - - then: - def e = thrown(SMBApiException.class) - e.statusCode == NtStatus.STATUS_SHARING_VIOLATION.value - share.list("").collect { it.fileName } contains "locked" - - cleanup: - lockedFile.close() - share.rm("locked") - } - - def "should transfer big file to share"() { - given: - def file = share.openFile("bigfile", EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null) - def bytes = new byte[32 * 1024 * 1024 + 10] - Random.newInstance().nextBytes(bytes) - def istream = new ByteArrayInputStream(bytes) - - when: - def ostream = file.getOutputStream(new LoggingProgressListener()) - try { - byte[] buffer = new byte[4096] - int len - while ((len = istream.read(buffer)) != -1) { - ostream.write(buffer, 0, len) - } - } finally { - istream.close() - ostream.close() - file.close() - } - - then: - share.fileExists("bigfile") - - when: - def readBytes = new byte[32 * 1024 * 1024 + 10] - def readFile = share.openFile("bigfile", EnumSet.of(AccessMask.FILE_READ_DATA), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - try { - def remoteIs = readFile.getInputStream(new LoggingProgressListener()) - try { - def offset = 0 - while (offset < readBytes.length) { - def read = remoteIs.read(readBytes, offset, readBytes.length - offset) - if (read > 0) { - offset += read - } else { - break - } - } - } finally { - remoteIs.close() - } - } finally { - readFile.close() - } - - then: - readBytes == bytes - - cleanup: - share.rm("bigfile") - } - - def "should lock and unlock the file"() { - given: - def fileToLock = share.openFile("fileToLock", EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, EnumSet.noneOf(SMB2ShareAccess.class), FILE_CREATE, null) - - when: - fileToLock.requestLock().exclusiveLock(0, 10, true).send() - - then: - noExceptionThrown() - - when: - fileToLock.requestLock().unlock(0, 10).send() - - then: - noExceptionThrown() - - cleanup: - fileToLock.close() - share.rm("fileToLock") - } - - def "should fail requesting overlapping exclusive lock range"() { - given: - def fileToLock = share.openFile("fileToLock", EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, EnumSet.noneOf(SMB2ShareAccess.class), FILE_CREATE, null) - - when: - fileToLock.requestLock().exclusiveLock(0, 10, true).send() - fileToLock.requestLock().exclusiveLock(5, 15, true).send() - - then: - thrown(SMBApiException.class) - - when: - fileToLock.requestLock().unlock(0, 10).send() - fileToLock.requestLock().exclusiveLock(5, 15, true).send() - - then: - noExceptionThrown() - - when: - fileToLock.requestLock().unlock(5, 15).send() - fileToLock.close() - - then: - noExceptionThrown() - - cleanup: - share.rm("fileToLock") - } - - def "should append to the file"() { - given: - def file = share.openFile("appendfile", EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN_IF, null) - def bytes = new byte[1024 * 1024] - Random.newInstance().nextBytes(bytes) - def istream = new ByteArrayInputStream(bytes) - - when: - def ostream = file.getOutputStream(new LoggingProgressListener()) - try { - byte[] buffer = new byte[4096] - int len - while ((len = istream.read(buffer)) != -1) { - ostream.write(buffer, 0, len) - } - } finally { - istream.close() - ostream.close() - file.close() - } - - then: - share.fileExists("appendfile") - - when: - def appendfile = share.openFile("appendfile", EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN_IF, null) - def bytes2 = new byte[1024 * 1024] - Random.newInstance().nextBytes(bytes2) - def istream2 = new ByteArrayInputStream(bytes2) - ostream = appendfile.getOutputStream(new LoggingProgressListener(), true) - try { - byte[] buffer = new byte[4096] - int len - while ((len = istream2.read(buffer)) != -1) { - ostream.write(buffer, 0, len) - } - } finally { - istream2.close() - ostream.close() - appendfile.close() - } - - then: - share.fileExists("appendfile") - - when: - def readBytes = new byte[2 * 1024 * 1024] - def readFile = share.openFile("appendfile", EnumSet.of(AccessMask.FILE_READ_DATA), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - try { - def remoteIs = readFile.getInputStream(new LoggingProgressListener()) - try { - def offset = 0 - while (offset < readBytes.length) { - def read = remoteIs.read(readBytes, offset, readBytes.length - offset) - if (read > 0) { - offset += read - } else { - break - } - } - } finally { - remoteIs.close() - } - } finally { - readFile.close() - } - - then: - readBytes == [bytes, bytes2].flatten() - - cleanup: - share.rm("appendfile") - } - - def "should be able to copy files remotely"() { - given: - def src = share.openFile("srcFile", EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_OVERWRITE_IF, null) - src.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)) - src.close() - - src = share.openFile("srcFile", EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - def dst = share.openFile("dstFile", EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null) - - when: - src.remoteCopyTo(dst) - - then: - share.fileExists("dstFile") - def srcSize = src.getFileInformation(FileStandardInformation.class).endOfFile - def dstSize = dst.getFileInformation(FileStandardInformation.class).endOfFile - srcSize == dstSize - - cleanup: - try { - share.rm("srcFile") - } catch (SMBApiException e) { - // Ignored - } - - try { - share.rm("dstFile") - } catch (SMBApiException e) { - // Ignored - } - } - - def "should correctly detect file existence"() { - given: - share.mkdir("im_a_directory") - def src = share.openFile("im_a_file", EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_OVERWRITE_IF, null) - src.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)) - src.close() - - expect: - share.fileExists("im_a_file") - !share.fileExists("im_a_directory") - !share.fileExists("i_do_not_exist") - - cleanup: - share.rm("im_a_file") - share.rmdir("im_a_directory", false) - } - - @Unroll - def "should not fail if #method response is DELETE_PENDING for file"() { - given: - def textFile = share.openFile("test.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_CREATE, null) - textFile.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)) - textFile.close() - textFile = share.openFile("test.txt", EnumSet.of(AccessMask.GENERIC_ALL), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - textFile.deleteOnClose() - - when: - func(share) - - then: - noExceptionThrown() - - where: - method | func - "rm" | { s -> s.rm("test.txt") } - "fileExists" | { s -> s.fileExists("test.txt") } - } - - def "should write async file"() { - given: - def size = 2 * 1024 * 1024 + 10 - def file = share.openFile("bigfile", EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null) - def bytes = new byte[size] - Random.newInstance().nextBytes(bytes) - def istream = new ByteArrayInputStream(bytes) - - when: - def writtenFuture = file.writeAsync(new InputStreamByteChunkProvider(istream)) - file.close() - istream.close() - - then: - writtenFuture.get() == size - - when: - def readBytes = new byte[size] - def readFile = share.openFile("bigfile", EnumSet.of(AccessMask.FILE_READ_DATA), null, SMB2ShareAccess.ALL, FILE_OPEN, null) - def read = 0 - for (;;) { - def nrRead = readFile.read(readBytes, read, read, size - read) - if (nrRead == -1) { - break - } - read += nrRead - } - - then: - read == size - readBytes == bytes - cleanup: - share.rm("bigfile") - - } - - def "should transfer file using GZIPOutputStream via InputStreamByteChunkProvider to SMB share"() { - given: -// def DATA_FILE = "dataFile.txt" -// def DATA_FILE_ZIPPED = "dataFile.zip" - def DATA_FILE = File.createTempFile("dataFile", "txt") - def DATA_FILE_ZIPPED = File.createTempFile("dataFile", "zip") - def SMB_FILE = "SMBFile.txt" - def SMB_FILE_ZIP = "SMBFile.zip" - def SMB_FILE_UNZIPPED = "SMBFileUnzipped.txt" - - def dst = share.openFile(SMB_FILE, EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null) - def dstZipped = share.openFile(SMB_FILE_ZIP, EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null) - def dstUnzipped = share.openFile(SMB_FILE_UNZIPPED, EnumSet.of(AccessMask.FILE_WRITE_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null) - - //Generate data which can be compressed - def localDataFile = new FileWriter(DATA_FILE) - try { - for (i in 0..10_000) { - localDataFile.append("HelloWorld") - } - } finally { - localDataFile.close() - } - - //Compress data file locally - def fis = new FileInputStream(DATA_FILE) - def fos = new FileOutputStream(DATA_FILE_ZIPPED) - def gzipOS = new GZIPOutputStream(fos) - try { - byte[] buffer = new byte[1024] - int len; - while ((len = fis.read(buffer)) != -1) { - gzipOS.write(buffer, 0, len) - } - } finally { - gzipOS.close() - fos.close() - fis.close() - } - - when: - //Write non-compressed file to SMB - dst.write(new InputStreamByteChunkProvider(new FileInputStream(DATA_FILE))) - //Write zipped file to SMB - dstZipped.write(new InputStreamByteChunkProvider(new FileInputStream(DATA_FILE_ZIPPED))) - //Unzip file using GZIPInputStream on the fly and write to SMB - dstUnzipped.write(new InputStreamByteChunkProvider(new GZIPInputStream(new FileInputStream(DATA_FILE_ZIPPED)))) - - then: - share.fileExists(SMB_FILE) - share.fileExists(SMB_FILE_ZIP) - share.fileExists(SMB_FILE_UNZIPPED) - - def dstSize = dst.getFileInformation(FileStandardInformation.class).endOfFile - def dstUnzippedSize = dstUnzipped.getFileInformation(FileStandardInformation.class).endOfFile - - //Neither size nor contents of file written to SMB from GZipInputStream match. - // SMB_FILE_UNZIPPED file arrives filled with '0x0' on SMB share - dstSize == dstUnzippedSize - - cleanup: - share.rm(SMB_FILE) - share.rm(SMB_FILE_ZIP) - share.rm(SMB_FILE_UNZIPPED) - DATA_FILE.delete() - DATA_FILE_ZIPPED.delete() - } -} diff --git a/src/it/java/com/hierynomus/smbj/AnonymousIntegrationTest.java b/src/it/java/com/hierynomus/smbj/AnonymousIntegrationTest.java index b9fd06eb..8df8575e 100644 --- a/src/it/java/com/hierynomus/smbj/AnonymousIntegrationTest.java +++ b/src/it/java/com/hierynomus/smbj/AnonymousIntegrationTest.java @@ -19,6 +19,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import com.hierynomus.mssmb2.SMB2Dialect; import com.hierynomus.security.bc.BCSecurityProvider; @@ -27,6 +29,7 @@ import com.hierynomus.smbj.session.Session; import com.hierynomus.smbj.share.DiskShare; import com.hierynomus.smbj.share.Share; +import com.hierynomus.smbj.testcontainers.SambaContainer; import static org.junit.jupiter.api.Assertions.*; @@ -34,37 +37,37 @@ import static com.hierynomus.smbj.testing.TestingUtils.*; +@Testcontainers public class AnonymousIntegrationTest { - private SmbConfig base = SmbConfig.builder().withDialects(SMB2Dialect.SMB_3_0).withEncryptData(true).withSigningRequired(false).withMultiProtocolNegotiate(true).withDfsEnabled(true).withSecurityProvider(new BCSecurityProvider()).build(); + @Container + private static final SambaContainer samba = new SambaContainer.Builder().build(); static Stream connectWith() { return Stream.of( - Arguments.of(config(SMB2Dialect.SMB_2_1, false, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_3_0, false, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_3_0, true, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_3_0_2, false, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_3_0_2, true, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_3_1_1, false, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_3_1_1, true, false), AuthenticationContext.anonymous()), - Arguments.of(config(SMB2Dialect.SMB_2_1, false, false), AuthenticationContext.guest()), - Arguments.of(config(SMB2Dialect.SMB_3_0, false, false), AuthenticationContext.guest()), - Arguments.of(config(SMB2Dialect.SMB_3_0, true, false), AuthenticationContext.guest()), - Arguments.of(config(SMB2Dialect.SMB_3_0_2, false, false), AuthenticationContext.guest()), - Arguments.of(config(SMB2Dialect.SMB_3_0_2, true, false), AuthenticationContext.guest()), - Arguments.of(config(SMB2Dialect.SMB_3_1_1, false, false), AuthenticationContext.guest()), - Arguments.of(config(SMB2Dialect.SMB_3_1_1, true, false), AuthenticationContext.guest()) + Arguments.of(SMB2Dialect.SMB_2_1, false, false, AuthenticationContext.anonymous()), + Arguments.of(SMB2Dialect.SMB_3_0, false, false, AuthenticationContext.anonymous()), + Arguments.of(SMB2Dialect.SMB_3_0, true, false, AuthenticationContext.anonymous()), + Arguments.of(SMB2Dialect.SMB_3_0_2, false, false, AuthenticationContext.anonymous()), + Arguments.of(SMB2Dialect.SMB_3_0_2, true, false, AuthenticationContext.anonymous()), + Arguments.of(SMB2Dialect.SMB_3_1_1, false, false, AuthenticationContext.anonymous()), + Arguments.of(SMB2Dialect.SMB_3_1_1, true, false, AuthenticationContext.anonymous()), + // Arguments.of(SMB2Dialect.SMB_2_1, false, false, AuthenticationContext.guest()), + Arguments.of(SMB2Dialect.SMB_3_0, false, false, AuthenticationContext.guest()), + Arguments.of(SMB2Dialect.SMB_3_0, true, false, AuthenticationContext.guest()), + Arguments.of(SMB2Dialect.SMB_3_0_2, false, false, AuthenticationContext.guest()), + Arguments.of(SMB2Dialect.SMB_3_0_2, true, false, AuthenticationContext.guest()), + Arguments.of(SMB2Dialect.SMB_3_1_1, false, false, AuthenticationContext.guest()), + Arguments.of(SMB2Dialect.SMB_3_1_1, true, false, AuthenticationContext.guest()) ); } - private static SmbConfig config(SMB2Dialect dialect, boolean encrypt, boolean signing) { - return SmbConfig.builder().withDialects(dialect).withEncryptData(encrypt).withSigningRequired(signing).withMultiProtocolNegotiate(true).withDfsEnabled(true).withSecurityProvider(new BCSecurityProvider()).build(); - } - - @ParameterizedTest + @ParameterizedTest(name = "Should authenticate using ({0}, {1}, {2}) with {3}") @MethodSource("connectWith") - public void shouldAuthenticate(SmbConfig base, AuthenticationContext authContext) throws Exception { - withConnectedClient(base, (connection) -> { + public void shouldAuthenticate(SMB2Dialect dialect, boolean encrypt, boolean sign, + AuthenticationContext authContext) throws Exception { + SmbConfig base = config(dialect, encrypt, sign); + samba.withConnectedClient(base, (connection) -> { try (Session session = connection.authenticate(authContext)) { assertNotNull(session.getSessionId()); } @@ -73,18 +76,20 @@ public void shouldAuthenticate(SmbConfig base, AuthenticationContext authContext @Test public void shouldFailConnectingAnonymousWhenSigningRequired() throws Exception { - SmbConfig config = SmbConfig.builder(base).withSigningRequired(true).build(); - assertThrows(SMB2GuestSigningRequiredException.class, () -> withConnectedClient(config, (connection) -> { + SmbConfig config = SmbConfig.builder().withDialects(SMB2Dialect.SMB_3_0).withEncryptData(true).withSigningRequired(true).withMultiProtocolNegotiate(true).withDfsEnabled(true).withSecurityProvider(new BCSecurityProvider()).build(); + assertThrows(SMB2GuestSigningRequiredException.class, () -> samba.withConnectedClient(config, (connection) -> { try (Session session = connection.authenticate(AuthenticationContext.anonymous())) { fail("Should not be able to connect"); } })); } - @ParameterizedTest + @ParameterizedTest(name = "Should connect to public share using ({0}, {1}, {2}) with {3}") @MethodSource("connectWith") - public void shouldConnectToPublicShare(SmbConfig base, AuthenticationContext authContext) throws Exception { - withConnectedClient(base, (connection) -> { + public void shouldConnectToPublicShare(SMB2Dialect dialect, boolean encrypt, boolean sign, + AuthenticationContext authContext) throws Exception { + SmbConfig base = config(dialect, encrypt, sign); + samba.withConnectedClient(base, (connection) -> { try (Session session = connection.authenticate(authContext)) { try (Share share = session.connectShare("public")) { assertInstanceOf(DiskShare.class, share); diff --git a/src/it/java/com/hierynomus/smbj/ChangeNotifyIntegrationTest.java b/src/it/java/com/hierynomus/smbj/ChangeNotifyIntegrationTest.java new file mode 100644 index 00000000..c67a93c0 --- /dev/null +++ b/src/it/java/com/hierynomus/smbj/ChangeNotifyIntegrationTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * 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 com.hierynomus.smbj; + +import static com.hierynomus.mssmb2.SMB2CompletionFilter.FILE_NOTIFY_CHANGE_FILE_NAME; +import static com.hierynomus.mssmb2.SMB2CompletionFilter.FILE_NOTIFY_CHANGE_LAST_WRITE; +import static com.hierynomus.mssmb2.SMB2CompletionFilter.FILE_NOTIFY_CHANGE_SIZE; +import static com.hierynomus.smbj.testing.TestingUtils.DEFAULT_AUTHENTICATION_CONTEXT; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.concurrent.Future; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.msfscc.FileAttributes; +import com.hierynomus.msfscc.FileNotifyAction; +import com.hierynomus.msfscc.directory.FileNotifyInformation; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; +import com.hierynomus.mssmb2.messages.SMB2ChangeNotifyResponse; +import com.hierynomus.protocol.commons.concurrent.Futures; +import com.hierynomus.smbj.common.SMBRuntimeException; +import com.hierynomus.smbj.io.ArrayByteChunkProvider; +import com.hierynomus.smbj.share.Directory; +import com.hierynomus.smbj.share.DiskShare; +import com.hierynomus.smbj.share.File; +import com.hierynomus.smbj.testcontainers.SambaContainer; + +@Testcontainers +public class ChangeNotifyIntegrationTest { + @Container + private static final SambaContainer samba = new SambaContainer.Builder().build(); + + @ParameterizedTest(name = "should watch changes") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldWatchChanges(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("directory"); + try (Directory d = share.openDirectory("directory", EnumSet.of(AccessMask.GENERIC_READ), + EnumSet.of(FileAttributes.FILE_ATTRIBUTE_DIRECTORY), SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + Future fut = d.watchAsync(EnumSet.of(FILE_NOTIFY_CHANGE_SIZE, + FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE), false); + try (File f = share.openFile("directory/TestNotify.txt", + EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE, AccessMask.DELETE), + EnumSet.of(FileAttributes.FILE_ATTRIBUTE_NORMAL), SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new ArrayByteChunkProvider("Testing 123 123".getBytes(StandardCharsets.UTF_8), 0)); + } + + assertThat(fut).isDone(); + SMB2ChangeNotifyResponse resp = fut.get(); + assertThat(resp.getFileNotifyInfoList()).isNotNull().size().isOne(); + FileNotifyInformation fni = resp.getFileNotifyInfoList().get(0); + assertThat(fni).extracting("action").isEqualTo(FileNotifyAction.FILE_ACTION_ADDED); + assertThat(fni).extracting("fileName").isEqualTo("TestNotify.txt"); + } + share.rmdir("directory", true); + } + }); + } + + @ParameterizedTest(name = "should cancel ChangeNotify") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldCancelChangeNotify(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("to_be_watched"); + try (Directory d = share.openDirectory("to_be_watched", EnumSet.of(AccessMask.GENERIC_ALL), + null, SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + d.deleteOnClose(); + Future fut = d.watchAsync(EnumSet.of(FILE_NOTIFY_CHANGE_FILE_NAME), true); + fut.cancel(true); + SMB2ChangeNotifyResponse resp = assertDoesNotThrow( + () -> Futures.get(fut, SMBRuntimeException.Wrapper)); + assertThat(resp).extracting("fileNotifyInfoList", as(InstanceOfAssertFactories.LIST)).isEmpty(); + } + } + }); + } + +} diff --git a/src/it/java/com/hierynomus/smbj/DfsIntegrationTest.java b/src/it/java/com/hierynomus/smbj/DfsIntegrationTest.java new file mode 100644 index 00000000..7c8cfdb6 --- /dev/null +++ b/src/it/java/com/hierynomus/smbj/DfsIntegrationTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * 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 com.hierynomus.smbj; + +import static com.hierynomus.smbj.testing.TestingUtils.DEFAULT_AUTHENTICATION_CONTEXT; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.EnumSet; +import java.util.List; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; +import com.hierynomus.smbj.share.Directory; +import com.hierynomus.smbj.share.DiskShare; +import com.hierynomus.smbj.testcontainers.SambaContainer; +import com.hierynomus.smbj.testing.TestingUtils.ConsumerWithError; + +@Testcontainers +public class DfsIntegrationTest { + @Container + private static final SambaContainer samba = new SambaContainer.Builder().build(); + + @ParameterizedTest(name = "should connect to DFS share") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#dfsConfig") + public void shouldConnectToDfsShare(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("dfs")) { + assertThat(share).isInstanceOf(DiskShare.class); + List files = share.list(""); + assertThat(files).map(FileIdBothDirectoryInformation::getFileName).contains("public", "user"); + } + }); + } + + @ParameterizedTest(name = "should list contents of DFS virtual directory") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#dfsConfig") + public void shouldListDfsVirtualDirectory(SmbConfig c) throws Exception { + withDir(c, "user", (dir) -> { + assertThat(dir.list()).map(FileIdBothDirectoryInformation::getFileName).contains("."); + }); + } + + @ParameterizedTest(name = "should connect to fallback if first link is broken") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#dfsConfig") + public void shouldConnectToFallbackIfFirstLinkBroken(SmbConfig c) throws Exception { + withDir(c, "firstfail-public", dir -> { + assertThat(dir.list()).map(FileIdBothDirectoryInformation::getFileName).contains("."); + }); + } + + @ParameterizedTest(name = "should have filename for regular directory share when dfs is enabled GH#603") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#dfsConfig") + public void shouldHaveFilenameForRegularDirectoryShareWhenDfsIsEnabled(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("a_directory"); + try (Directory dir = share.openDirectory("a_directory", EnumSet.of(AccessMask.GENERIC_READ), null, + EnumSet.of(SMB2ShareAccess.FILE_SHARE_READ), SMB2CreateDisposition.FILE_OPEN, null)) { + + assertThat(dir.getPath()).isEqualTo("a_directory"); + assertThat(dir.getFileName()).isEqualTo("\\\\localhost\\user\\a_directory"); + assertThat(dir.getUncPath()).isEqualTo("\\\\localhost\\user\\a_directory"); + } + share.rmdir("a_directory", false); + } + }); + } + + private void withDir(SmbConfig c, String name, ConsumerWithError f) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("dfs")) { + assertThat(share).isInstanceOf(DiskShare.class); + try (Directory dir = share.openDirectory(name, EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + f.accept(dir); + } + } + }); + } +} diff --git a/src/it/java/com/hierynomus/smbj/IntegrationTest.java b/src/it/java/com/hierynomus/smbj/IntegrationTest.java new file mode 100644 index 00000000..c49d019b --- /dev/null +++ b/src/it/java/com/hierynomus/smbj/IntegrationTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * 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 com.hierynomus.smbj; + +import static com.hierynomus.smbj.testing.TestingUtils.DEFAULT_AUTHENTICATION_CONTEXT; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation; +import com.hierynomus.smbj.share.DiskShare; +import com.hierynomus.smbj.share.Share; +import com.hierynomus.smbj.testcontainers.SambaContainer; + +@Testcontainers +public class IntegrationTest { + @Container + private static final SambaContainer samba = new SambaContainer.Builder().build(); + + @ParameterizedTest(name = "should connect") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldConnect(SmbConfig c) throws Exception { + samba.withConnectedClient(c, (connection) -> { + assertThat(connection).extracting("connected", as(InstanceOfAssertFactories.BOOLEAN)).isTrue(); + }); + } + + @ParameterizedTest(name = "should authenticate") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldAuthenticate(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + assertThat(session).extracting("sessionId").isNotNull(); + }); + } + + @ParameterizedTest(name = "should connect to share") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldConnectToShare(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (Share share = session.connectShare("public")) { + assertThat(share).isInstanceOf(DiskShare.class); + assertThat(share).extracting("treeId").isNotNull(); + assertThat(share).extracting("connected", as(InstanceOfAssertFactories.BOOLEAN)).isTrue(); + } + }); + } + + @ParameterizedTest(name = "should list contents of empty share") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldListContentsOfEmptyShare(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + List items = share.list(""); + assertThat(items).hasSize(2).map(FileIdBothDirectoryInformation::getFileName).contains(".", ".."); + } + }); + } + + @Test + public void shouldListContentsOfShareWithNullPath() throws Exception { + SmbConfig c = SmbConfig.builder().withMultiProtocolNegotiate(true).build(); + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + List items = share.list(null); + assertThat(items).hasSize(2).map(FileIdBothDirectoryInformation::getFileName).contains(".", ".."); + } + }); + } + + @ParameterizedTest(name = "should not fail closing connection twice") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldNotFailClosingConnectionTwice(SmbConfig c) throws Exception { + samba.withConnectedClient(c, (connection) -> { + assertDoesNotThrow(() -> connection.close()); + assertDoesNotThrow(() -> connection.close()); + }); + } + +} diff --git a/src/it/java/com/hierynomus/smbj/SMB2DirectoryIntegrationTest.java b/src/it/java/com/hierynomus/smbj/SMB2DirectoryIntegrationTest.java new file mode 100644 index 00000000..ff4a7e35 --- /dev/null +++ b/src/it/java/com/hierynomus/smbj/SMB2DirectoryIntegrationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * 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 com.hierynomus.smbj; + +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.List; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; +import com.hierynomus.smbj.io.ArrayByteChunkProvider; +import com.hierynomus.smbj.share.Directory; +import com.hierynomus.smbj.share.DiskEntry; +import com.hierynomus.smbj.share.DiskShare; +import com.hierynomus.smbj.share.File; +import com.hierynomus.smbj.testcontainers.SambaContainer; + +import static com.hierynomus.smbj.testing.TestingUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@Testcontainers +public class SMB2DirectoryIntegrationTest { + + @Container + private static final SambaContainer samba = new SambaContainer.Builder().build(); + + @ParameterizedTest(name = "should open directory") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldOpenDirectory(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("public")) { + try (DiskEntry d = share.openDirectory("folder", EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + assertThat(d).isNotNull().isInstanceOf(Directory.class); + } + } + }); + } + + @ParameterizedTest(name = "should check directory exists") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldCheckDirectoryExists(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("im_a_directory"); + try (File src = share.openFile("im_a_file", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null)) { + src.write(new ArrayByteChunkProvider("Hello World".getBytes(StandardCharsets.UTF_8), 0)); + } + + assertThat(share.folderExists("im_a_directory")).isTrue(); + assertThat(share.folderExists("does_not_exist")).isFalse(); + assertThat(share.folderExists("im_a_file")).isFalse(); + + share.rm("im_a_file"); + share.rmdir("im_a_directory", false); + } + }); + } + + @ParameterizedTest(name = "should list directory contents") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldListDirectoryContents(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("public")) { + List items = share.list("folder"); + assertThat(items).size().isGreaterThan(0); + } + }); + } + + @ParameterizedTest(name = "should not fail if 'rmdir' response is DELETE_PENDING for directory") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldNotFailIfRmdirResponseIsDeletePendingForDirectory(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + Directory newDir = share.openDirectory("to_be_removed", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null); + newDir.close(); + try (Directory d = share.openDirectory("to_be_removed", EnumSet.of(AccessMask.GENERIC_ALL), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + d.deleteOnClose(); + assertDoesNotThrow(() -> share.rmdir("to_be_removed", false)); + } + } + }); + } + + @ParameterizedTest(name = "should not fail if 'folderExists' response is DELETE_PENDING for directory") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldNotFailIfFolderExistsResponseIsDeletePendingForDirectory(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + Directory newDir = share.openDirectory("to_be_removed", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null); + newDir.close(); + try (Directory d = share.openDirectory("to_be_removed", EnumSet.of(AccessMask.GENERIC_ALL), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + d.deleteOnClose(); + assertDoesNotThrow(() -> share.folderExists("to_be_removed")); + } + } + }); + } + + @ParameterizedTest(name = "should create Directory and list contents") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#loggedIn") + public void shouldCreateDirectoryAndListContents(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("testdir"); + List items = share.list(""); + assertThat(items).map(FileIdBothDirectoryInformation::getFileName).contains("testdir"); + assertThat(share.list("testdir")).hasSize(2).map(FileIdBothDirectoryInformation::getFileName) + .contains(".", ".."); + share.rmdir("testdir", true); + } + }); + } +} diff --git a/src/it/java/com/hierynomus/smbj/SMB2FileIntegrationTest.java b/src/it/java/com/hierynomus/smbj/SMB2FileIntegrationTest.java new file mode 100644 index 00000000..dc3a8aba --- /dev/null +++ b/src/it/java/com/hierynomus/smbj/SMB2FileIntegrationTest.java @@ -0,0 +1,465 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * 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 com.hierynomus.smbj; + +import static com.hierynomus.smbj.testing.TestingUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.FileWriter; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.InstanceOfAssertFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.mserref.NtStatus; +import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation; +import com.hierynomus.msfscc.fileinformation.FileIdFullDirectoryInformation; +import com.hierynomus.msfscc.fileinformation.FileInternalInformation; +import com.hierynomus.msfscc.fileinformation.FileStandardInformation; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; +import com.hierynomus.mssmb2.SMBApiException; +import com.hierynomus.smbj.io.ArrayByteChunkProvider; +import com.hierynomus.smbj.io.InputStreamByteChunkProvider; +import com.hierynomus.smbj.share.DiskEntry; +import com.hierynomus.smbj.share.DiskShare; +import com.hierynomus.smbj.share.File; +import com.hierynomus.smbj.testcontainers.SambaContainer; +import com.hierynomus.smbj.testing.LoggingProgressListener; +import com.hierynomus.smbj.testing.TestingUtils; + +@Testcontainers +public class SMB2FileIntegrationTest { + + @Container + private static final SambaContainer samba = new SambaContainer.Builder().build(); + + @ParameterizedTest(name = "should open file") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldOpenFile(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("public")) { + try (DiskEntry f = share.open("test.txt", EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + assertThat(f).isNotNull().isInstanceOf(File.class); + } + } + }); + } + + @ParameterizedTest(name = "should create File 'test.txt'") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldCreateFileAndListContentsOfShare(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File f = share.openFile("test.txt", EnumSet.of(AccessMask.GENERIC_ALL), null, SMB2ShareAccess.ALL, + null, null)) { + List items = share.list(""); + assertThat(items).map(FileIdBothDirectoryInformation::getFileName).contains("test.txt"); + f.deleteOnClose(); + } + } + }); + } + + @ParameterizedTest(name = "should check file exists") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldCheckFileExists(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("im_a_directory"); + try (File src = share.openFile("im_a_file", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OVERWRITE_IF, null)) { + src.write(new ArrayByteChunkProvider("Hello World".getBytes(StandardCharsets.UTF_8), 0)); + } + + assertThat(share.fileExists("im_a_file")).isTrue(); + assertThat(share.fileExists("im_a_directory")).isFalse(); + assertThat(share.fileExists("does_not_exist")).isFalse(); + + share.rm("im_a_file"); + share.rmdir("im_a_directory", false); + } + }); + } + + @ParameterizedTest(name = "should read file contents of file in directory") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldReadFileContentsOfFileInDirectory(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + share.mkdir("dirForFile"); + String filePath = "dirForFile\\" + TestingUtils.randomFileName(); + try (File f = share.openFile(filePath, EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)); + } + + try (File f = share.openFile(filePath, EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + try (InputStream is = f.getInputStream()) { + byte[] buf = new byte[1024]; + int read = is.read(buf); + assertThat(new String(buf, 0, read, StandardCharsets.UTF_8)).isEqualTo("Hello World!"); + } + } + + share.rmdir("dirForFile", true); + } + }); + } + + @ParameterizedTest(name = "should not delete locked file until closed") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldNotDeleteLockedFile(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File f = share.openFile("locked", EnumSet.of(AccessMask.GENERIC_WRITE), null, + EnumSet.noneOf(SMB2ShareAccess.class), + SMB2CreateDisposition.FILE_CREATE, null)) { + SMBApiException ex = assertThrows(SMBApiException.class, () -> share.rm("locked")); + assertThat(ex.getStatusCode()).isEqualTo(NtStatus.STATUS_SHARING_VIOLATION.getValue()); + assertThat(share.list("")).map(FileIdBothDirectoryInformation::getFileName).contains("locked"); + } + share.rm("locked"); + } + }); + } + + @ParameterizedTest(name = "should transfer big file to share") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldTransferBigFileToShare(SmbConfig c) throws Exception { + byte[] bytes = new byte[10 * 1024 * 1024 + 10]; + TestingUtils.RANDOM.nextBytes(bytes); + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + if (share.fileExists("bigfile")) { + share.rm("bigfile"); + } + try (File f = share.openFile("bigfile", EnumSet.of(AccessMask.GENERIC_WRITE), null, + EnumSet.noneOf(SMB2ShareAccess.class), + SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new ArrayByteChunkProvider(bytes, 0)); + } + + try (File f = share.openFile("bigfile", EnumSet.of(AccessMask.GENERIC_READ), null, + EnumSet.noneOf(SMB2ShareAccess.class), + SMB2CreateDisposition.FILE_OPEN, null)) { + int offset = 0; + byte[] buf = new byte[1024]; + try (InputStream is = f.getInputStream()) { + int read; + while ((read = is.read(buf)) != -1) { + assertThat(buf).startsWith(Arrays.copyOfRange(bytes, offset, offset + read)); + offset += read; + } + } + } + + share.rm("bigfile"); + } + }); + } + + @ParameterizedTest(name = "should lock and unlock a file") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldLockAndUnlockFile(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File fileToLock = share.openFile("fileToLock", + EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, + EnumSet.noneOf(SMB2ShareAccess.class), SMB2CreateDisposition.FILE_CREATE, null)) { + assertDoesNotThrow(() -> fileToLock.requestLock().exclusiveLock(0, 10, true).send()); + assertDoesNotThrow(() -> fileToLock.requestLock().unlock(0, 10).send()); + } + + share.rm("fileToLock"); + } + }); + } + + @ParameterizedTest(name = "should fail to lock overlapping ranges") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldFailToLockOverlappingRange(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File fileToLock = share.openFile("fileToLock", + EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, + EnumSet.noneOf(SMB2ShareAccess.class), SMB2CreateDisposition.FILE_CREATE, null)) { + assertDoesNotThrow(() -> fileToLock.requestLock().exclusiveLock(0, 10, true).send()); + assertThrows(SMBApiException.class, + () -> fileToLock.requestLock().exclusiveLock(5, 15, true).send()); + assertDoesNotThrow(() -> fileToLock.requestLock().unlock(0, 10).send()); + assertDoesNotThrow(() -> fileToLock.requestLock().exclusiveLock(5, 15, true).send()); + assertDoesNotThrow(() -> fileToLock.requestLock().unlock(5, 15).send()); + } + share.rm("fileToLock"); + } + }); + } + + @ParameterizedTest(name = "should be able to read fileID from FileIdBothDirectoryInformation") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldReadFileIDFromFileIdBothDirectoryInformation(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("public")) { + List infoList = share.list("folder"); + assertThat(infoList).isNotEmpty(); + FileIdBothDirectoryInformation first = infoList.stream().filter((i) -> !i.getFileName().startsWith(".")) + .findFirst().orElseThrow(); + try (DiskEntry f = share.open("folder\\" + first.getFileName(), EnumSet.of(AccessMask.GENERIC_READ), + null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + FileInternalInformation fif = f.getFileInformation(FileInternalInformation.class); + assertThat(first.getFileId()).isEqualTo(fif.getIndexNumber()); + } + } + }); + } + + @ParameterizedTest(name = "should be able to read fileID from FileIdFullDirectoryInformation") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldReadFileIDFromFileIdFullDirectoryInformation(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("public")) { + List infoList = share.list("folder", + FileIdFullDirectoryInformation.class); + assertThat(infoList).isNotEmpty(); + FileIdFullDirectoryInformation first = infoList.stream().filter((i) -> !i.getFileName().startsWith(".")) + .findFirst().orElseThrow(); + try (DiskEntry f = share.open("folder\\" + first.getFileName(), EnumSet.of(AccessMask.GENERIC_READ), + null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + FileInternalInformation fif = f.getFileInformation(FileInternalInformation.class); + assertThat(first.getFileId()).isEqualTo(fif.getIndexNumber()); + } + } + }); + } + + @ParameterizedTest(name = "should append to a file") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldAppendToFile(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File f = share.openFile("append.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + try (OutputStream os = f.getOutputStream(new LoggingProgressListener())) { + os.write("Hello World!".getBytes(StandardCharsets.UTF_8)); + } + } + + assertThat(share.fileExists("append.txt")).isTrue(); + + try (File f = share.openFile("append.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + try (OutputStream os = f.getOutputStream(new LoggingProgressListener(), true)) { + os.write("\nGoodbye World!".getBytes(StandardCharsets.UTF_8)); + } + } + + try (File f = share.openFile("append.txt", EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + try (InputStream is = f.getInputStream()) { + byte[] buf = new byte[1024]; + int read = is.read(buf); + assertThat(new String(buf, 0, read, StandardCharsets.UTF_8)) + .isEqualTo("Hello World!\nGoodbye World!"); + } + } + + share.rm("append.txt"); + } + }); + } + + @ParameterizedTest(name = "should copy files remotely") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldCopyFilesRemotely(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File f = share.openFile("srcfile.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)); + } + + try (File src = share.openFile("srcfile.txt", EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + try (File dst = share.openFile("dstfile.txt", EnumSet.of(AccessMask.FILE_WRITE_DATA), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + src.remoteCopyTo(dst); + + assertThat(share.fileExists("dstfile.txt")).isTrue(); + + assertThat(endOfFile(src)).isEqualTo(endOfFile(dst)); + } + } + + share.rm("srcfile.txt"); + share.rm("dstfile.txt"); + } + }); + } + + @ParameterizedTest(name = "should not fail if 'rm' response is DELETE_PENDING for file") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldNotFileIfRmResponseIsDeletePendingForFile(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File newFile = share.openFile("to_be_removed", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + newFile.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)); + } + try (File f = share.openFile("to_be_removed", EnumSet.of(AccessMask.GENERIC_ALL), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + f.deleteOnClose(); + + assertDoesNotThrow(() -> share.rm("to_be_removed")); + } + } + }); + } + + @ParameterizedTest(name = "should not fail if 'fileExists' response is DELETE_PENDING for file") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldNotFileIfFileExistsResponseIsDeletePendingForFile(SmbConfig c) throws Exception { + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File newFile = share.openFile("to_be_removed", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + newFile.write(new ArrayByteChunkProvider("Hello World!".getBytes(StandardCharsets.UTF_8), 0)); + } + try (File f = share.openFile("to_be_removed", EnumSet.of(AccessMask.GENERIC_ALL), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, null)) { + f.deleteOnClose(); + + assertDoesNotThrow(() -> share.fileExists("to_be_removed")); + } + } + }); + } + + @ParameterizedTest(name = "should write async file") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldWriteAsyncFile(SmbConfig c) throws Exception { + byte[] bytes = new byte[2 * 1024 * 1024 + 10]; + TestingUtils.RANDOM.nextBytes(bytes); + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, session -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + Future fut = null; + try (File f = share.openFile("bigfile", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OVERWRITE_IF, null)) { + fut = f.writeAsync(new InputStreamByteChunkProvider(new ByteArrayInputStream(bytes))); + } + + assertThat(fut).succeedsWithin(5, TimeUnit.SECONDS, InstanceOfAssertFactories.LONG) + .isEqualTo(bytes.length); + + try (File f = share.openFile("bigfile", EnumSet.of(AccessMask.GENERIC_READ), null, + EnumSet.noneOf(SMB2ShareAccess.class), + SMB2CreateDisposition.FILE_OPEN, null)) { + int offset = 0; + byte[] buf = new byte[1024]; + try (InputStream is = f.getInputStream()) { + int read; + while ((read = is.read(buf)) != -1) { + assertThat(buf).startsWith(Arrays.copyOfRange(bytes, offset, offset + read)); + offset += read; + } + } + } + + share.rm("bigfile"); + } + }); + } + + @ParameterizedTest(name = "should transfer file using GZipOutputStream via InputStreamByteChunkProvider to SMB Share") + @MethodSource("com.hierynomus.smbj.testing.TestingUtils#defaultTestingConfig") + public void shouldTransferAndUnzipFileInOneGo(SmbConfig c) throws Exception { + java.io.File dataFile = java.io.File.createTempFile("datafile", "txt"); + try (FileWriter fw = new FileWriter(dataFile)) { + for (int i = 0; i < 10000; i++) { + fw.write("Hello World!\n"); + } + } + + java.io.File zipFile = java.io.File.createTempFile("datafile", "txt.gz"); + try (InputStream is = new java.io.FileInputStream(dataFile); + OutputStream os = new java.io.FileOutputStream(zipFile)) { + try (GZIPOutputStream gos = new GZIPOutputStream(os)) { + IOUtils.copy(is, gos); + } + } + + samba.withAuthenticatedClient(c, DEFAULT_AUTHENTICATION_CONTEXT, (session) -> { + try (DiskShare share = (DiskShare) session.connectShare("user")) { + try (File f = share.openFile("datafile.txt.gz", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new InputStreamByteChunkProvider(new java.io.FileInputStream(zipFile))); + } + try (File f = share.openFile("datafile.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new InputStreamByteChunkProvider(new java.io.FileInputStream(dataFile))); + } + try (File f = share.openFile("unzipped.txt", EnumSet.of(AccessMask.GENERIC_WRITE), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_CREATE, null)) { + f.write(new InputStreamByteChunkProvider( + new GZIPInputStream(new java.io.FileInputStream(zipFile)))); + } + + assertThat(share.fileExists("datafile.txt.gz")).isTrue(); + assertThat(share.fileExists("datafile.txt")).isTrue(); + assertThat(share.fileExists("unzipped.txt")).isTrue(); + + try (File orig = share.openFile("datafile.txt", EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + try (File unzipped = share.openFile("unzipped.txt", EnumSet.of(AccessMask.GENERIC_READ), null, + SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null)) { + assertThat(endOfFile(orig)).isEqualTo(endOfFile(unzipped)); + } + } + + share.rm("datafile.txt.gz"); + share.rm("datafile.txt"); + share.rm("unzipped.txt"); + } + }); + + dataFile.delete(); + zipFile.delete(); + } +} diff --git a/src/it/java/com/hierynomus/smbj/testcontainers/SambaContainer.java b/src/it/java/com/hierynomus/smbj/testcontainers/SambaContainer.java new file mode 100644 index 00000000..0501ad1d --- /dev/null +++ b/src/it/java/com/hierynomus/smbj/testcontainers/SambaContainer.java @@ -0,0 +1,130 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * 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 com.hierynomus.smbj.testcontainers; + +import java.nio.file.Paths; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; +import org.testcontainers.utility.DockerLoggerFactory; + +import com.hierynomus.smbj.SMBClient; +import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.testing.TestingUtils.ConsumerWithError; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; + +public class SambaContainer extends GenericContainer { + /** + * A workaround for strange logger names of testcontainers. They contain no + * dots, but contain slashes, + * square brackets, and even emoji. It's uneasy to set the logging level via the + * XML file of logback, the + * result would be less readable than the code below. + */ + public static class DebugLoggingImageFromDockerfile extends ImageFromDockerfile { + public DebugLoggingImageFromDockerfile() { + super(); + Logger logger = (Logger) LoggerFactory.getILoggerFactory() + .getLogger(DockerLoggerFactory.getLogger(getDockerImageName()).getName()); + logger.setLevel(Level.DEBUG); + } + } + + public static class Builder implements Consumer { + @Override + public void accept(DockerfileBuilder t) { + t.from("alpine:3.18.3"); + t.run("apk update && apk add --no-cache tini samba samba-common-tools supervisor bash"); + t.env("SMB_USER", "smbj"); + t.env("SMB_PASSWORD", "smbj"); + t.copy("smb.conf", "/etc/samba/smb.conf"); + t.copy("supervisord.conf", "/etc/supervisord.conf"); + t.copy("entrypoint.sh", "/entrypoint.sh"); + t.add("public", "/opt/samba/share"); + t.run("mkdir -p /opt/samba/readonly /opt/samba/user /opt/samba/dfs" + + " && chmod 777 /opt/samba/readonly /opt/samba/user /opt/samba/dfs" + + " && adduser -s /bin/false $SMB_USER -D $SMB_PASSWORD" + + " && (echo $SMB_PASSWORD; echo $SMB_PASSWORD ) | pdbedit -a -u $SMB_USER" + + " && chmod ugo+x /entrypoint.sh"); + t.expose(445); + t.entryPoint("/sbin/tini", "/entrypoint.sh"); + t.cmd("supervisord"); + } + + public SambaContainer build() { + return new SambaContainer(buildInner()); + } + + Future buildInner() { + return new DebugLoggingImageFromDockerfile() + .withDockerfileFromBuilder(this) + .withFileFromPath(".", Paths.get("src/it/docker-image")); + } + } + + + public SambaContainer(SambaContainer.Builder builder) { + this(builder.buildInner()); + } + + public SambaContainer(Future imageName) { + super(imageName); + withExposedPorts(445); + addFixedExposedPort(445, 445); + setWaitStrategy(Wait.forListeningPort()); + withLogConsumer(outputFrame -> { + switch (outputFrame.getType()) { + case STDOUT: + logger().info("sshd stdout: {}", outputFrame.getUtf8String().stripTrailing()); + break; + case STDERR: + logger().info("sshd stderr: {}", outputFrame.getUtf8String().stripTrailing()); + break; + default: + break; + } + }); + } + + + public void withConnectedClient(SmbConfig config, ConsumerWithError f) throws Exception { + try (SMBClient client = new SMBClient(config)) { + try (Connection connection = client.connect(getHost(), getFirstMappedPort())) { + f.accept(connection); + } + } + } + + + public void withAuthenticatedClient(SmbConfig config, AuthenticationContext ctx, + ConsumerWithError f) throws Exception { + withConnectedClient(config, (connection) -> { + try (Session session = connection.authenticate(ctx)) { + f.accept(session); + } + }); + } +} diff --git a/src/it/groovy/com/hierynomus/smbj/LoggingProgressListener.groovy b/src/it/java/com/hierynomus/smbj/testing/LoggingProgressListener.java similarity index 56% rename from src/it/groovy/com/hierynomus/smbj/LoggingProgressListener.groovy rename to src/it/java/com/hierynomus/smbj/testing/LoggingProgressListener.java index 37da7e44..072afaf7 100644 --- a/src/it/groovy/com/hierynomus/smbj/LoggingProgressListener.groovy +++ b/src/it/java/com/hierynomus/smbj/testing/LoggingProgressListener.java @@ -13,15 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.hierynomus.smbj +package com.hierynomus.smbj.testing; -import org.slf4j.LoggerFactory +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -class LoggingProgressListener implements ProgressListener { - def logger = LoggerFactory.getLogger(LoggingProgressListener) +import com.hierynomus.smbj.ProgressListener; + +public class LoggingProgressListener implements ProgressListener { + private static final Logger logger = LoggerFactory.getLogger(LoggingProgressListener.class); + + @Override + public void onProgressChanged(long numBytes, long totalBytes) { + logger.info("R/W {} bytes, total = {} bytes", numBytes, totalBytes); + } - @Override - void onProgressChanged(long numBytes, long totalBytes) { - logger.info("R/W {} bytes, total = {} bytes", numBytes, totalBytes) - } } diff --git a/src/it/java/com/hierynomus/smbj/testing/TestingUtils.java b/src/it/java/com/hierynomus/smbj/testing/TestingUtils.java index 6016f701..2f047bde 100644 --- a/src/it/java/com/hierynomus/smbj/testing/TestingUtils.java +++ b/src/it/java/com/hierynomus/smbj/testing/TestingUtils.java @@ -15,17 +15,74 @@ */ package com.hierynomus.smbj.testing; -import com.hierynomus.smbj.SMBClient; +import java.util.Random; +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +import com.hierynomus.msfscc.fileinformation.FileStandardInformation; +import com.hierynomus.mssmb2.SMB2Dialect; +import com.hierynomus.security.bc.BCSecurityProvider; import com.hierynomus.smbj.SmbConfig; -import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.share.File; public class TestingUtils { - public static void withConnectedClient(SmbConfig config, ConsumerWithError f) throws Exception { - try (SMBClient client = new SMBClient(config)) { - try (Connection connection = client.connect("127.0.0.1")) { - f.accept(connection); - } - } + public static final Random RANDOM = new Random(); + + public static final AuthenticationContext DEFAULT_AUTHENTICATION_CONTEXT = new AuthenticationContext("smbj", + "smbj".toCharArray(), null); + + public static SmbConfig config(SMB2Dialect dialect, boolean encrypt, boolean signing) { + return SmbConfig.builder().withDialects(dialect).withEncryptData(encrypt).withSigningRequired(signing) + .withMultiProtocolNegotiate(true).withDfsEnabled(true).withSecurityProvider(new BCSecurityProvider()) + .build(); + } + + public static Stream allValidDialectCombinations() { + return Stream.of( + config(SMB2Dialect.SMB_2_1, false, false), + config(SMB2Dialect.SMB_2_1, false, true), + config(SMB2Dialect.SMB_3_0, false, false), + config(SMB2Dialect.SMB_3_0, false, true), + config(SMB2Dialect.SMB_3_0, true, false), + config(SMB2Dialect.SMB_3_0, true, true), + config(SMB2Dialect.SMB_3_0_2, false, false), + config(SMB2Dialect.SMB_3_0_2, false, true), + config(SMB2Dialect.SMB_3_0_2, true, false), + config(SMB2Dialect.SMB_3_0_2, true, true), + config(SMB2Dialect.SMB_3_1_1, false, false), + config(SMB2Dialect.SMB_3_1_1, false, true), + config(SMB2Dialect.SMB_3_1_1, true, false), + config(SMB2Dialect.SMB_3_1_1, true, true)); + } + + public static Stream validConfigs() { + return allValidDialectCombinations().map(c -> { + return Arguments.of(c); + }); + } + + public static Stream defaultTestingConfig() { + return Stream.of(Arguments.of(config(SMB2Dialect.SMB_3_1_1, true, true))); + } + public static Stream dfsConfig() { + return Stream.of(Arguments.of(config(SMB2Dialect.SMB_2_1, false, true))); + } + + public static Stream loggedIn() { + return allValidDialectCombinations().map(c -> { + return Arguments.of(c, DEFAULT_AUTHENTICATION_CONTEXT); + }); + } + + + public static String randomFileName() { + return "test-" + RANDOM.nextInt(1000000) + ".txt"; + } + + public static long endOfFile(File f) { + return f.getFileInformation(FileStandardInformation.class).getEndOfFile(); } public interface ConsumerWithError { diff --git a/src/test/java/com/hierynomus/smbj/connection/ConnectionTest.java b/src/test/java/com/hierynomus/smbj/connection/ConnectionTest.java index f1c10d35..a6541701 100644 --- a/src/test/java/com/hierynomus/smbj/connection/ConnectionTest.java +++ b/src/test/java/com/hierynomus/smbj/connection/ConnectionTest.java @@ -43,7 +43,7 @@ public class ConnectionTest { private static SmbConfig config(PacketProcessor processor) { return SmbConfig.builder() - .withTransportLayerFactory(new StubTransportLayerFactory(new DefaultPacketProcessor().wrap(processor))) + .withTransportLayerFactory(new StubTransportLayerFactory<>(new DefaultPacketProcessor().wrap(processor))) .withAuthenticators(new StubAuthenticator.Factory()).build(); }