├── .gitignore ├── .travis.yml ├── .editorconfig ├── LICENSE ├── src ├── test │ └── java │ │ └── com │ │ └── github │ │ └── stefanbirkner │ │ └── fakesftpserver │ │ └── rule │ │ ├── Executor.java │ │ └── FakeSftpServerRuleTest.java └── main │ └── java │ └── com │ └── github │ └── stefanbirkner │ └── fakesftpserver │ └── rule │ └── FakeSftpServerRule.java ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .flattened-pom.xml 2 | target 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: mvn -DskipTests -Dgpg.skip install 3 | jdk: 4 | - openjdk8 5 | script: mvn test javadoc:javadoc 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Configuration file for EditorConfig: http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | continuation_indent_size = 4 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Stefan Birkner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/test/java/com/github/stefanbirkner/fakesftpserver/rule/Executor.java: -------------------------------------------------------------------------------- 1 | package com.github.stefanbirkner.fakesftpserver.rule; 2 | 3 | import com.github.stefanbirkner.fishbowl.Statement; 4 | import org.junit.rules.TestRule; 5 | import org.junit.runner.Description; 6 | 7 | import static com.github.stefanbirkner.fishbowl.Fishbowl.*; 8 | 9 | class Executor { 10 | private static final Description DUMMY_DESCRIPTION = null; 11 | 12 | static void executeTestWithRule( 13 | Statement test, 14 | TestRule rule 15 | ) { 16 | wrapCheckedException(executeTestWithRuleRaw(test, rule)); 17 | } 18 | 19 | static void executeTestThatThrowsExceptionWithRule( 20 | Statement test, 21 | TestRule rule 22 | ) { 23 | ignoreException( 24 | executeTestWithRuleRaw(test, rule), 25 | Throwable.class 26 | ); 27 | } 28 | 29 | private static Statement executeTestWithRuleRaw( 30 | Statement test, 31 | TestRule rule 32 | ) { 33 | org.junit.runners.model.Statement statement 34 | = new org.junit.runners.model.Statement() { 35 | @Override 36 | public void evaluate() throws Throwable { 37 | test.evaluate(); 38 | } 39 | }; 40 | return () -> rule.apply(statement, DUMMY_DESCRIPTION).evaluate(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.github.stefanbirkner 7 | lib-parent 8 | 12 9 | 10 | 11 | fake-sftp-server-rule 12 | 2.1.0-SNAPSHOT 13 | jar 14 | 15 | Fake SFTP Server Rule 16 | Fake SFTP Server Rule is a JUnit rule that runs an in-memory SFTP server. 17 | https://github.com/stefanbirkner/fake-sftp-server-rule/ 18 | 2016 19 | 20 | 21 | MIT License 22 | http://opensource.org/licenses/MIT 23 | repo 24 | 25 | 26 | 27 | 28 | scm:git:git://github.com/stefanbirkner/fake-sftp-server-rule.git 29 | scm:git:git@github.com:stefanbirkner/fake-sftp-server-rule.git 30 | https://github.com/stefanbirkner/fake-sftp-server-rule/ 31 | 32 | 33 | 34 | 1.8 35 | 1.8 36 | 37 | 38 | 39 | 40 | com.github.marschall 41 | memoryfilesystem 42 | [0.8.0,) 43 | 44 | 45 | org.apache.sshd 46 | sshd-core 47 | [1,2) 48 | 49 | 50 | junit 51 | junit-dep 52 | [4.9,) 53 | 54 | 55 | com.github.stefanbirkner 56 | fishbowl 57 | [1.4.1] 58 | test 59 | 60 | 61 | com.jcraft 62 | jsch 63 | [0.1.54] 64 | test 65 | 66 | 67 | commons-io 68 | commons-io 69 | [2.6] 70 | test 71 | 72 | 73 | org.assertj 74 | assertj-core 75 | [3.9.1] 76 | test 77 | 78 | 79 | org.slf4j 80 | slf4j-nop 81 | [1.7.25] 82 | test 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fake SFTP Server Rule 2 | 3 | [![Build Status](https://travis-ci.org/stefanbirkner/fake-sftp-server-rule.svg?branch=master)](https://travis-ci.org/stefanbirkner/fake-sftp-server-rule) 4 | 5 | Fake SFTP Server Rule is a JUnit rule that runs an in-memory SFTP server while 6 | your tests are running. It uses the SFTP server of the 7 | [Apache SSHD](http://mina.apache.org/sshd-project/index.html) project. 8 | 9 | Fake SFTP Server Rule is published under the 10 | [MIT license](http://opensource.org/licenses/MIT). It requires at least Java 8. 11 | Please 12 | [open an issue](https://github.com/stefanbirkner/fake-sftp-server-rule/issues/new) 13 | if you want to use it with an older version of Java. 14 | 15 | I want to thank my former team SAM at ThoughtWorks for using this library and 16 | @crizzis, @OArtyomov and @TheSentinel454 for their feature requests. 17 | 18 | There is an alternative to Fake SFTP Server Rule that is independent of the 19 | test framework. Its name is 20 | [Fake SFTP Server Lambda](https://github.com/stefanbirkner/fake-sftp-server-lambda). 21 | 22 | ## Installation 23 | 24 | Fake SFTP Server Rule is available from 25 | [Maven Central](https://search.maven.org/#search|ga|1|fake-sftp-server-rule). 26 | 27 | 28 | com.github.stefanbirkner 29 | fake-sftp-server-rule 30 | 2.0.1 31 | 32 | 33 | If you upgrade from a version < 2.x to the newest version please read the last 34 | section of this readme. 35 | 36 | ## Usage 37 | 38 | The Fake SFTP Server Rule is used by adding it to your test class. 39 | 40 | import com.github.stefanbirkner.fakesftpserver.rule.FakeSftpServerRule; 41 | 42 | public class TestClass { 43 | @Rule 44 | public final FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 45 | 46 | ... 47 | } 48 | 49 | This rule starts a server before your test and stops it afterwards. 50 | 51 | By default the SFTP server listens on an auto-allocated port. During the test 52 | this port can be obtained by `sftpServer.getPort()`. It can be changed 53 | by calling `setPort(int)`. If you do this from within a test then the server 54 | gets restarted. The time-consuming restart can be avoided by setting the port 55 | immediately after creating the rule. 56 | 57 | public class TestClass { 58 | @Rule 59 | public final FakeSftpServerRule sftpServer = new FakeSftpServerRule() 60 | .setPort(1234); 61 | 62 | ... 63 | } 64 | 65 | You can interact with the SFTP server by using the SFTP protocol with password 66 | authentication. By default the server accepts every pair of username and 67 | password, but you can restrict it to specific pairs. 68 | 69 | public class TestClass { 70 | @Rule 71 | public final FakeSftpServerRule sftpServer = new FakeSftpServerRule() 72 | .addUser("username", "password"); 73 | 74 | ... 75 | } 76 | 77 | It is also possible to do this during the test using the same method. 78 | 79 | ### Testing code that reads files 80 | 81 | If you test code that reads files from an SFTP server then you need a server 82 | that provides these files. Fake SFTP Server Rule provides a shortcut for 83 | uploading files to the server. 84 | 85 | @Test 86 | public void testTextFile() { 87 | sftpServer.putFile("/directory/file.txt", "content of file", UTF_8); 88 | //code that downloads the file 89 | } 90 | 91 | @Test 92 | public void testBinaryFile() { 93 | byte[] content = createContent(); 94 | sftpServer.putFile("/directory/file.bin", content); 95 | //code that downloads the file 96 | } 97 | 98 | Test data that is provided as an input stream can be uploaded directly from that 99 | input stream. This is very handy if your test data is available as a resource. 100 | 101 | @Test 102 | public void testFileFromInputStream() { 103 | InputStream is = getClass().getResourceAsStream("data.bin"); 104 | sftpServer.putFile("/directory/file.bin", is); 105 | //code that downloads the file 106 | } 107 | 108 | If you need an empty directory then you can use the method 109 | `createDirectory(String)`. 110 | 111 | @Test 112 | public void testDirectory() { 113 | sftpServer.createDirectory("/a/directory"); 114 | //code that reads from or writes to that directory 115 | } 116 | 117 | You may create multiple directories at once with `createDirectories(String...)`. 118 | 119 | @Test 120 | public void testDirectories() { 121 | sftpServer.createDirectories( 122 | "/a/directory", 123 | "/another/directory" 124 | ); 125 | //code that reads from or writes to that directories 126 | } 127 | 128 | 129 | ### Testing code that writes files 130 | 131 | If you test code that writes files to an SFTP server then you need to verify 132 | the upload. Fake SFTP Server Rule provides a shortcut for getting the file's 133 | content from the server. 134 | 135 | @Test 136 | public void testTextFile() { 137 | //code that uploads the file 138 | String fileContent = sftpServer.getFileContent("/directory/file.txt", UTF_8); 139 | ... 140 | } 141 | 142 | @Test 143 | public void testBinaryFile() { 144 | //code that uploads the file 145 | byte[] fileContent = sftpServer.getFileContent("/directory/file.bin"); 146 | ... 147 | } 148 | 149 | ### Testing existence of files 150 | 151 | If you want to check whether a file hast been created or deleted then you can 152 | verify that it exists or not. 153 | 154 | @Test 155 | public void testFile() { 156 | //code that uploads or deletes the file 157 | boolean exists = sftpServer.existsFile("/directory/file.txt"); 158 | ... 159 | } 160 | 161 | The method returns `true` iff the file exists and it is not a directory. 162 | 163 | ### Delete all files 164 | 165 | If you want to reuse the SFTP server then you can delete all files and 166 | directories on the SFTP server. (This is rarely necessary because the rule 167 | itself takes care that every test starts and ends with a clean SFTP server.) 168 | 169 | sftpServer.deleteAllFilesAndDirectories() 170 | 171 | ## Contributing 172 | 173 | You have three options if you have a feature request, found a bug or 174 | simply have a question about Fake SFTP Server Rule. 175 | 176 | * [Write an issue.](https://github.com/stefanbirkner/fake-sftp-server-rule/issues/new) 177 | * Create a pull request. (See [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/index.html)) 178 | * [Write a mail to mail@stefan-birkner.de](mailto:mail@stefan-birkner.de) 179 | 180 | 181 | ## Development Guide 182 | 183 | Fake SFTP Server Rule is build with [Maven](http://maven.apache.org/). If you 184 | want to contribute code then 185 | 186 | * Please write a test for your change. 187 | * Ensure that you didn't break the build by running `mvn verify -Dgpg.skip`. 188 | * Fork the repo and create a pull request. (See [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/index.html)) 189 | 190 | The basic coding style is described in the 191 | [EditorConfig](http://editorconfig.org/) file `.editorconfig`. 192 | 193 | Fake SFTP Server Rule supports [Travis CI](https://travis-ci.org/) for 194 | continuous integration. Your pull request will be automatically build by Travis 195 | CI. 196 | 197 | 198 | ## Release Guide 199 | 200 | * Select a new version according to the 201 | [Semantic Versioning 2.0.0 Standard](http://semver.org/). 202 | * Set the new version in `pom.xml` and in the `Installation` section of 203 | this readme. 204 | * Commit the modified `pom.xml` and `README.md`. 205 | * Run `mvn clean deploy` with JDK 8. 206 | * Add a tag for the release: `git tag fake-sftp-server-rule-X.X.X` 207 | 208 | 209 | ## Upgrading from 0.x.y or 1.x.y to version >= 2 210 | 211 | In older versions the SFTP server listened to port 23454 by default. From 212 | version 2 on it selects an arbitrary free port by default. If your tests fail 213 | after an upgrade you may consider to restore the old behaviour by immediately 214 | setting the old port after creating the rule. 215 | 216 | @Rule 217 | public final FakeSftpServerRule sftpServer = new FakeSftpServerRule() 218 | .setPort(23454); 219 | -------------------------------------------------------------------------------- /src/main/java/com/github/stefanbirkner/fakesftpserver/rule/FakeSftpServerRule.java: -------------------------------------------------------------------------------- 1 | package com.github.stefanbirkner.fakesftpserver.rule; 2 | 3 | import org.apache.sshd.server.SshServer; 4 | import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; 5 | import org.apache.sshd.server.session.ServerSession; 6 | import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; 7 | import org.junit.rules.TestRule; 8 | import org.junit.runner.Description; 9 | import org.junit.runners.model.Statement; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.nio.charset.Charset; 14 | import java.nio.file.*; 15 | import java.nio.file.attribute.BasicFileAttributes; 16 | import java.nio.file.attribute.UserPrincipalLookupService; 17 | import java.nio.file.spi.FileSystemProvider; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.Objects; 21 | import java.util.Set; 22 | 23 | import static com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder.newLinux; 24 | import static java.nio.file.FileVisitResult.CONTINUE; 25 | import static java.nio.file.Files.*; 26 | import static java.util.Collections.singletonList; 27 | 28 | /** 29 | * Fake SFTP Server Rule is a JUnit rule that runs an in-memory SFTP server 30 | * while your tests are running. 31 | *

The Fake SFTP Server Rule is used by adding it to your test class. 32 | *

 33 |  * public class TestClass {
 34 |  *   @Rule
 35 |  *   public final FakeSftpServerRule sftpServer = new FakeSftpServerRule();
 36 |  *
 37 |  *   ...
 38 |  * }
 39 |  * 
40 | *

This rule starts a server before your test and stops it afterwards. 41 | *

By default the SFTP server listens on an auto-allocated port. During the 42 | * test this port can be obtained by {@link #getPort() sftpServer.getPort()}. It 43 | * can be changed by calling {@link #setPort(int)}. If you do this from within a 44 | * test then the server gets restarted. The time-consuming restart can be 45 | * avoided by setting the port immediately after creating the rule. 46 | *

 47 |  * public class TestClass {
 48 |  *   @Rule
 49 |  *   public final FakeSftpServerRule sftpServer = new FakeSftpServerRule()
 50 |  *       .setPort(1234);
 51 |  *
 52 |  *   ...
 53 |  * }
 54 |  * 
55 | *

You can interact with the SFTP server by using the SFTP protocol with 56 | * password authentication. By default the server accepts every pair of 57 | * username and password, buy you can restrict it to specific pairs. 58 | *

 59 |  * public class TestClass {
 60 |  *   @Rule
 61 |  *   public final FakeSftpServerRule sftpServer = new FakeSftpServerRule()
 62 |  *       .{@link #addUser(String, String) addUser}("username", "password");
 63 |  *
 64 |  *   ...
 65 |  * }
 66 |  * 
67 | *

It is also possible to do this during the test using the same method. 68 | * 69 | *

Testing code that reads files

70 | *

If you test code that reads files from an SFTP server then you need the 71 | * server to provide these files. Fake SFTP Server Rule has a shortcut for 72 | * uploading files to the server. 73 | *

 74 |  * @Test
 75 |  * public void testTextFile() {
 76 |  *   {@link #putFile(String, String, Charset) sftpServer.putFile}("/directory/file.txt", "content of file", UTF_8);
 77 |  *   //code that downloads the file
 78 |  * }
 79 |  *
 80 |  * @Test
 81 |  * public void testBinaryFile() {
 82 |  *   byte[] content = createContent();
 83 |  *   {@link #putFile(String, byte[]) sftpServer.putFile}("/directory/file.bin", content);
 84 |  *   //code that downloads the file
 85 |  * }
 86 |  * 
87 | *

Test data that is provided as an input stream can be uploaded directly 88 | * from that input stream. This is very handy if your test data is available as 89 | * a resource. 90 | *

 91 |  * @Test
 92 |  * public void testFileFromInputStream() {
 93 |  *   InputStream is = getClass().getResourceAsStream("data.bin");
 94 |  *   {@link #putFile(String, InputStream) sftpServer.putFile}("/directory/file.bin", is);
 95 |  *   //code that downloads the file
 96 |  * }
 97 |  * 
98 | *

If you need an empty directory then you can use the method 99 | * {@link #createDirectory(String)}. 100 | *

101 |  * @Test
102 |  * public void testDirectory() {
103 |  *   sftpServer.{@link #createDirectory(String) createDirectory}("/a/directory");
104 |  *   //code that reads from or writes to that directory
105 |  * }
106 |  * 
107 | *

You may create multiple directories at once with 108 | * {@link #createDirectories(String...)}. 109 | *

110 |  * @Test
111 |  * public void testDirectories() {
112 |  *   sftpServer.{@link #createDirectories(String...) createDirectories}(
113 |  *     "/a/directory",
114 |  *     "/another/directory"
115 |  *   );
116 |  *   //code that reads from or writes to that directories
117 |  * }
118 |  * 
119 | *

Testing code that writes files

120 | *

If you test code that writes files to an SFTP server then you need to 121 | * verify the upload. Fake SFTP Server Rule provides a shortcut for getting the 122 | * file's content from the server. 123 | *

124 |  * @Test
125 |  * public void testTextFile() {
126 |  *   //code that uploads the file
127 |  *   String fileContent = {@link #getFileContent(String, Charset) sftpServer.getFileContent}("/directory/file.txt", UTF_8);
128 |  *   ...
129 |  * }
130 |  *
131 |  * @Test
132 |  * public void testBinaryFile() {
133 |  *   //code that uploads the file
134 |  *   byte[] fileContent = {@link #getFileContent(String) sftpServer.getFileContent}("/directory/file.bin");
135 |  *   ...
136 |  * }
137 |  * 
138 | * 139 | *

Testing existence of files

140 | *

If you want to check whether a file hast been created or deleted then you 141 | * can verify that it exists or not. 142 | *

143 |  * @Test
144 |  * public void testFile() {
145 |  *   //code that uploads or deletes the file
146 |  *   boolean exists = {@link #existsFile(String) sftpServer.existsFile}("/directory/file.txt");
147 |  *   ...
148 |  * }
149 |  * 
150 | *

The method returns {@code true} iff the file exists and it is not a directory. 151 | * 152 | *

Delete all files

153 | *

If you want to reuse the SFTP server then you can delete all files and 154 | * directories on the SFTP server. (This is rarely necessary because the rule 155 | * itself takes care that every test starts and ends with a clean SFTP server.) 156 | *

{@link #deleteAllFilesAndDirectories() sftpServer.deleteAllFilesAndDirectories()};
157 | */ 158 | public class FakeSftpServerRule implements TestRule { 159 | private static final SimpleFileVisitor DELETE_FILES_AND_DIRECTORIES 160 | = new SimpleFileVisitor() { 161 | @Override 162 | public FileVisitResult visitFile( 163 | Path file, 164 | BasicFileAttributes attrs 165 | ) throws IOException { 166 | delete(file); 167 | return CONTINUE; 168 | } 169 | 170 | @Override 171 | public FileVisitResult postVisitDirectory( 172 | Path dir, 173 | IOException exc 174 | ) throws IOException { 175 | if (dir.getParent() != null) 176 | delete(dir); 177 | return super.postVisitDirectory(dir, exc); 178 | } 179 | }; 180 | private final Map usernamesAndPasswords = new HashMap<>(); 181 | private int port = 0; 182 | 183 | private FileSystem fileSystem; 184 | private SshServer server; 185 | 186 | /** 187 | * Returns the port of the SFTP server. If the SFTP server listens on an 188 | * auto-allocated port (that means you didn't call {@link #setPort(int)}) 189 | * then you can only call this method during the test. 190 | * 191 | * @return the port of the SFTP server. 192 | * @throws IllegalStateException if you call the method outside of a test 193 | * but haven't called {@link #setPort(int)}) before. 194 | */ 195 | public int getPort() { 196 | if (port == 0) 197 | return getPortFromServer(); 198 | else 199 | return port; 200 | } 201 | 202 | private int getPortFromServer() { 203 | verifyThatTestIsRunning("call getPort()"); 204 | return server.getPort(); 205 | } 206 | 207 | /** 208 | * Set the port of the SFTP server. The SFTP server gets restarted if you 209 | * call {@code setPort} from within a test. The time-consuming restart can 210 | * be avoided by setting the port immediately after creating the rule. 211 | * @param port the port. Must be between 1 and 65535. 212 | * @return the rule itself. 213 | * @throws IllegalArgumentException if the port is not between 1 and 65535. 214 | * @throws IllegalStateException if the server cannot be restarted. 215 | */ 216 | public FakeSftpServerRule setPort( 217 | int port 218 | ) { 219 | if (port < 1 || port > 65535) 220 | throw new IllegalArgumentException( 221 | "Port cannot be set to " + port 222 | + " because only ports between 1 and 65535 are valid." 223 | ); 224 | this.port = port; 225 | if (server != null) 226 | restartServer(); 227 | return this; 228 | } 229 | 230 | /** 231 | * Register a username with its password. After registering a username 232 | * it is only possible to connect to the server with one of the registered 233 | * username/password pairs. 234 | *

If {@code addUser} is called multiple times with the same username but 235 | * different passwords then the last password is effective. 236 | * @param username the username. 237 | * @param password the password for the specified username. 238 | * @return the rule itself. 239 | */ 240 | public FakeSftpServerRule addUser( 241 | String username, 242 | String password 243 | ) { 244 | usernamesAndPasswords.put(username, password); 245 | return this; 246 | } 247 | 248 | private void restartServer() { 249 | try { 250 | server.stop(); 251 | startServer(fileSystem); 252 | } catch (IOException e) { 253 | throw new IllegalStateException( 254 | "The SFTP server cannot be restarted.", 255 | e 256 | ); 257 | } 258 | } 259 | 260 | /** 261 | * Put a text file on the SFTP folder. The file is available by the 262 | * specified path. 263 | * @param path the path to the file. 264 | * @param content the files content. 265 | * @param encoding the encoding of the file. 266 | * @throws IOException if the file cannot be written. 267 | */ 268 | public void putFile( 269 | String path, 270 | String content, 271 | Charset encoding 272 | ) throws IOException { 273 | byte[] contentAsBytes = content.getBytes(encoding); 274 | putFile(path, contentAsBytes); 275 | } 276 | 277 | /** 278 | * Put a file on the SFTP folder. The file is available by the specified 279 | * path. 280 | * @param path the path to the file. 281 | * @param content the files content. 282 | * @throws IOException if the file cannot be written. 283 | */ 284 | public void putFile( 285 | String path, 286 | byte[] content 287 | ) throws IOException { 288 | verifyThatTestIsRunning("upload file"); 289 | Path pathAsObject = fileSystem.getPath(path); 290 | ensureDirectoryOfPathExists(pathAsObject); 291 | write(pathAsObject, content); 292 | } 293 | 294 | /** 295 | * Put a file on the SFTP folder. The file is available by the specified 296 | * path. The file content is read from an {@code InputStream}. 297 | * @param path the path to the file. 298 | * @param is an {@code InputStream} that provides the file's content. 299 | * @throws IOException if the file cannot be written or the input stream 300 | * cannot be read. 301 | */ 302 | public void putFile( 303 | String path, 304 | InputStream is 305 | ) throws IOException { 306 | verifyThatTestIsRunning("upload file"); 307 | Path pathAsObject = fileSystem.getPath(path); 308 | ensureDirectoryOfPathExists(pathAsObject); 309 | copy(is, pathAsObject); 310 | } 311 | 312 | /** 313 | * Create a directory on the SFTP server. 314 | * @param path the directory's path. 315 | * @throws IOException if the directory cannot be created. 316 | */ 317 | public void createDirectory( 318 | String path 319 | ) throws IOException { 320 | verifyThatTestIsRunning("create directory"); 321 | Path pathAsObject = fileSystem.getPath(path); 322 | Files.createDirectories(pathAsObject); 323 | } 324 | 325 | /** 326 | * Create multiple directories on the SFTP server. 327 | * @param paths the directories' paths. 328 | * @throws IOException if at least one directory cannot be created. 329 | */ 330 | public void createDirectories( 331 | String... paths 332 | ) throws IOException { 333 | for (String path: paths) 334 | createDirectory(path); 335 | } 336 | 337 | /** 338 | * Get a text file from the SFTP server. The file is decoded using the 339 | * specified encoding. 340 | * @param path the path to the file. 341 | * @param encoding the file's encoding. 342 | * @return the content of the text file. 343 | * @throws IOException if the file cannot be read. 344 | * @throws IllegalStateException if not called from within a test. 345 | */ 346 | public String getFileContent( 347 | String path, 348 | Charset encoding 349 | ) throws IOException { 350 | byte[] content = getFileContent(path); 351 | return new String(content, encoding); 352 | } 353 | 354 | /** 355 | * Get a file from the SFTP server. 356 | * @param path the path to the file. 357 | * @return the content of the file. 358 | * @throws IOException if the file cannot be read. 359 | * @throws IllegalStateException if not called from within a test. 360 | */ 361 | public byte[] getFileContent( 362 | String path 363 | ) throws IOException { 364 | verifyThatTestIsRunning("download file"); 365 | Path pathAsObject = fileSystem.getPath(path); 366 | return readAllBytes(pathAsObject); 367 | } 368 | 369 | /** 370 | * Checks the existence of a file. returns {@code true} iff the file exists 371 | * and it is not a directory. 372 | * @param path the path to the file. 373 | * @return {@code true} iff the file exists and it is not a directory. 374 | * @throws IllegalStateException if not called from within a test. 375 | */ 376 | public boolean existsFile( 377 | String path 378 | ) { 379 | verifyThatTestIsRunning("check existence of file"); 380 | Path pathAsObject = fileSystem.getPath(path); 381 | return exists(pathAsObject) && !isDirectory(pathAsObject); 382 | } 383 | 384 | /** 385 | * Deletes all files and directories. 386 | * @throws IOException if an I/O error is thrown while deleting the files 387 | * and directories 388 | */ 389 | public void deleteAllFilesAndDirectories() throws IOException { 390 | for (Path directory: fileSystem.getRootDirectories()) 391 | walkFileTree(directory, DELETE_FILES_AND_DIRECTORIES); 392 | } 393 | 394 | @Override 395 | public Statement apply( 396 | Statement base, 397 | Description description 398 | ) { 399 | return new Statement() { 400 | @Override 401 | public void evaluate() throws Throwable { 402 | try ( 403 | FileSystem fileSystem = createFileSystem() 404 | ) { 405 | startServer(fileSystem); 406 | try { 407 | base.evaluate(); 408 | } finally { 409 | server.stop(); 410 | server = null; 411 | } 412 | } finally { 413 | fileSystem = null; 414 | } 415 | } 416 | }; 417 | } 418 | 419 | private FileSystem createFileSystem( 420 | ) throws IOException { 421 | fileSystem = newLinux().build("FakeSftpServerRule@" + hashCode()); 422 | return fileSystem; 423 | } 424 | 425 | private SshServer startServer( 426 | FileSystem fileSystem 427 | ) throws IOException { 428 | SshServer server = SshServer.setUpDefaultServer(); 429 | server.setPort(port); 430 | server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider()); 431 | server.setPasswordAuthenticator(this::authenticate); 432 | server.setSubsystemFactories(singletonList(new SftpSubsystemFactory())); 433 | /* When a channel is closed SshServer calls close() on the file system. 434 | * In order to use the file system for multiple channels/sessions we 435 | * have to use a file system wrapper whose close() does nothing. 436 | */ 437 | server.setFileSystemFactory(session -> new DoNotClose(fileSystem)); 438 | server.start(); 439 | this.server = server; 440 | return server; 441 | } 442 | 443 | private boolean authenticate( 444 | String username, 445 | String password, 446 | ServerSession session 447 | ) { 448 | return usernamesAndPasswords.isEmpty() 449 | || Objects.equals( 450 | usernamesAndPasswords.get(username), 451 | password 452 | ); 453 | } 454 | 455 | private void ensureDirectoryOfPathExists( 456 | Path path 457 | ) throws IOException { 458 | Path directory = path.getParent(); 459 | if (directory != null && !directory.equals(path.getRoot())) 460 | Files.createDirectories(directory); 461 | } 462 | 463 | private void verifyThatTestIsRunning( 464 | String mode 465 | ) { 466 | if (fileSystem == null) 467 | throw new IllegalStateException( 468 | "Failed to " + mode + " because test has not been started or" 469 | + " is already finished." 470 | ); 471 | } 472 | 473 | private static class DoNotClose extends FileSystem { 474 | final FileSystem fileSystem; 475 | 476 | DoNotClose( 477 | FileSystem fileSystem 478 | ) { 479 | this.fileSystem = fileSystem; 480 | } 481 | 482 | @Override 483 | public FileSystemProvider provider() { 484 | return fileSystem.provider(); 485 | } 486 | 487 | @Override 488 | public void close( 489 | ) throws IOException { 490 | //will not be closed 491 | } 492 | 493 | @Override 494 | public boolean isOpen() { 495 | return fileSystem.isOpen(); 496 | } 497 | 498 | @Override 499 | public boolean isReadOnly() { 500 | return fileSystem.isReadOnly(); 501 | } 502 | 503 | @Override 504 | public String getSeparator() { 505 | return fileSystem.getSeparator(); 506 | } 507 | 508 | @Override 509 | public Iterable getRootDirectories() { 510 | return fileSystem.getRootDirectories(); 511 | } 512 | 513 | @Override 514 | public Iterable getFileStores() { 515 | return fileSystem.getFileStores(); 516 | } 517 | 518 | @Override 519 | public Set supportedFileAttributeViews() { 520 | return fileSystem.supportedFileAttributeViews(); 521 | } 522 | 523 | @Override 524 | public Path getPath( 525 | String first, 526 | String... more 527 | ) { 528 | return fileSystem.getPath(first, more); 529 | } 530 | 531 | @Override 532 | public PathMatcher getPathMatcher( 533 | String syntaxAndPattern 534 | ) { 535 | return fileSystem.getPathMatcher(syntaxAndPattern); 536 | } 537 | 538 | @Override 539 | public UserPrincipalLookupService getUserPrincipalLookupService() { 540 | return fileSystem.getUserPrincipalLookupService(); 541 | } 542 | 543 | @Override 544 | public WatchService newWatchService() throws IOException { 545 | return fileSystem.newWatchService(); 546 | } 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /src/test/java/com/github/stefanbirkner/fakesftpserver/rule/FakeSftpServerRuleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.stefanbirkner.fakesftpserver.rule; 2 | 3 | 4 | import com.jcraft.jsch.*; 5 | import org.apache.commons.io.IOUtils; 6 | import org.assertj.core.api.ThrowableAssert.ThrowingCallable; 7 | import org.junit.Test; 8 | import org.junit.experimental.runners.Enclosed; 9 | import org.junit.runner.RunWith; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.net.ConnectException; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | import java.util.Vector; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | 20 | import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestThatThrowsExceptionWithRule; 21 | import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestWithRule; 22 | import static com.github.stefanbirkner.fishbowl.Fishbowl.exceptionThrownBy; 23 | import static java.nio.charset.StandardCharsets.UTF_8; 24 | import static org.apache.commons.io.IOUtils.toByteArray; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 27 | import static org.assertj.core.api.Assertions.catchThrowable; 28 | 29 | /* Wording according to the draft: 30 | * http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13 31 | */ 32 | @RunWith(Enclosed.class) 33 | public class FakeSftpServerRuleTest { 34 | private static final byte[] DUMMY_CONTENT = new byte[]{1, 4, 2, 4, 2, 4}; 35 | private static final int DUMMY_PORT = 46354; 36 | private static final InputStream DUMMY_STREAM = new ByteArrayInputStream(DUMMY_CONTENT); 37 | private static final JSch JSCH = new JSch(); 38 | private static final int TIMEOUT = 500; 39 | 40 | public static class round_trip { 41 | @Test 42 | public void a_file_that_is_written_to_the_SFTP_server_can_be_read() { 43 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 44 | executeTestWithRule( 45 | () -> { 46 | Session session = connectToServer(sftpServer); 47 | ChannelSftp channel = connectSftpChannel(session); 48 | channel.put( 49 | new ByteArrayInputStream( 50 | "dummy content".getBytes(UTF_8) 51 | ), 52 | "dummy_file.txt" 53 | ); 54 | InputStream file = channel.get("dummy_file.txt"); 55 | assertThat(IOUtils.toString(file, UTF_8)) 56 | .isEqualTo("dummy content"); 57 | channel.disconnect(); 58 | session.disconnect(); 59 | }, 60 | sftpServer 61 | ); 62 | } 63 | } 64 | 65 | public static class connection { 66 | 67 | @Test 68 | public void multiple_connections_to_the_server_are_possible() { 69 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 70 | executeTestWithRule( 71 | () -> { 72 | connectAndDisconnect(sftpServer); 73 | connectAndDisconnect(sftpServer); 74 | }, 75 | sftpServer 76 | ); 77 | } 78 | 79 | @Test 80 | public void a_client_can_connect_to_the_server_at_a_user_specified_port() { 81 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 82 | .setPort(8394); 83 | executeTestWithRule( 84 | () -> connectToServerAtPort(8394), 85 | sftpServer 86 | ); 87 | } 88 | } 89 | 90 | @RunWith(Enclosed.class) 91 | public static class authentication { 92 | public static class server_without_credentials { 93 | @Test 94 | public void the_server_accepts_connections_with_password() { 95 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 96 | executeTestWithRule( 97 | () -> { 98 | Session session = createSessionWithCredentials( 99 | sftpServer, 100 | "dummy user", 101 | "dummy password" 102 | ); 103 | session.connect(TIMEOUT); 104 | }, 105 | sftpServer 106 | ); 107 | } 108 | } 109 | 110 | public static class server_with_credentials_immediately_set { 111 | @Test 112 | public void the_server_accepts_connections_with_correct_password() { 113 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 114 | .addUser("dummy user", "dummy password"); 115 | executeTestWithRule( 116 | () -> { 117 | Session session = createSessionWithCredentials( 118 | sftpServer, 119 | "dummy user", 120 | "dummy password" 121 | ); 122 | session.connect(TIMEOUT); 123 | }, 124 | sftpServer 125 | ); 126 | } 127 | 128 | 129 | @Test 130 | public void the_server_rejects_connections_with_wrong_password() { 131 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 132 | .addUser("dummy user", "correct password"); 133 | executeTestWithRule( 134 | () -> { 135 | Session session = createSessionWithCredentials( 136 | sftpServer, 137 | "dummy user", 138 | "wrong password" 139 | ); 140 | assertAuthenticationFails( 141 | () -> session.connect(TIMEOUT) 142 | ); 143 | }, 144 | sftpServer 145 | ); 146 | } 147 | 148 | @Test 149 | public void the_last_password_is_effective_if_addUser_is_called_multiple_times() { 150 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 151 | .addUser("dummy user", "first password") 152 | .addUser("dummy user", "second password"); 153 | executeTestWithRule( 154 | () -> { 155 | Session session = createSessionWithCredentials( 156 | sftpServer, 157 | "dummy user", 158 | "second password" 159 | ); 160 | session.connect(TIMEOUT); 161 | }, 162 | sftpServer 163 | ); 164 | } 165 | } 166 | 167 | public static class server_with_credentials_set_during_test { 168 | @Test 169 | public void the_server_accepts_connections_with_correct_password() { 170 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 171 | executeTestWithRule( 172 | () -> { 173 | sftpServer.addUser("dummy user", "dummy password"); 174 | Session session = createSessionWithCredentials( 175 | sftpServer, 176 | "dummy user", 177 | "dummy password" 178 | ); 179 | session.connect(TIMEOUT); 180 | }, 181 | sftpServer 182 | ); 183 | } 184 | 185 | @Test 186 | public void the_server_rejects_connections_with_wrong_password() { 187 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 188 | executeTestWithRule( 189 | () -> { 190 | sftpServer.addUser("dummy user", "correct password"); 191 | Session session = createSessionWithCredentials( 192 | sftpServer, 193 | "dummy user", 194 | "wrong password" 195 | ); 196 | assertAuthenticationFails( 197 | () -> session.connect(TIMEOUT) 198 | ); 199 | }, 200 | sftpServer 201 | ); 202 | } 203 | 204 | @Test 205 | public void the_last_password_is_effective_if_addUser_is_called_multiple_times() { 206 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 207 | executeTestWithRule( 208 | () -> { 209 | sftpServer 210 | .addUser("dummy user", "first password") 211 | .addUser("dummy user", "second password"); 212 | Session session = createSessionWithCredentials( 213 | sftpServer, 214 | "dummy user", 215 | "second password" 216 | ); 217 | session.connect(TIMEOUT); 218 | }, 219 | sftpServer 220 | ); 221 | } 222 | } 223 | 224 | private static Session createSessionWithCredentials( 225 | FakeSftpServerRule sftpServer, 226 | String username, 227 | String password 228 | ) throws JSchException { 229 | return FakeSftpServerRuleTest.createSessionWithCredentials( 230 | username, password, sftpServer.getPort() 231 | ); 232 | } 233 | 234 | private static void assertAuthenticationFails( 235 | ThrowingCallable connectToServer 236 | ) { 237 | assertThatThrownBy(connectToServer) 238 | .isInstanceOf(JSchException.class) 239 | .hasMessage("Auth fail"); 240 | } 241 | } 242 | 243 | @RunWith(Enclosed.class) 244 | public static class file_upload { 245 | public static class a_text_file { 246 | @Test 247 | public void that_is_put_to_root_directory_via_the_rule_can_be_read_from_server() { 248 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 249 | executeTestWithRule( 250 | () -> { 251 | sftpServer.putFile( 252 | "/dummy_file.txt", 253 | "dummy content with umlaut ü", 254 | UTF_8 255 | ); 256 | byte[] file = downloadFile(sftpServer, "/dummy_file.txt"); 257 | assertThat(new String(file, UTF_8)) 258 | .isEqualTo("dummy content with umlaut ü"); 259 | }, 260 | sftpServer 261 | ); 262 | } 263 | 264 | @Test 265 | public void that_is_put_to_directory_via_the_rule_can_be_read_from_server() { 266 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 267 | executeTestWithRule( 268 | () -> { 269 | sftpServer.putFile( 270 | "/dummy_directory/dummy_file.txt", 271 | "dummy content with umlaut ü", 272 | UTF_8 273 | ); 274 | byte[] file = downloadFile( 275 | sftpServer, 276 | "/dummy_directory/dummy_file.txt" 277 | ); 278 | assertThat(new String(file, UTF_8)) 279 | .isEqualTo("dummy content with umlaut ü"); 280 | }, 281 | sftpServer 282 | ); 283 | } 284 | 285 | @Test 286 | public void cannot_be_put_before_the_test_is_started() { 287 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 288 | Throwable exception = exceptionThrownBy( 289 | () -> sftpServer.putFile( 290 | "/dummy_file.txt", "dummy content", UTF_8 291 | ) 292 | ); 293 | assertThat(exception) 294 | .isInstanceOf(IllegalStateException.class) 295 | .hasMessage( 296 | "Failed to upload file because test has not been started" 297 | + " or is already finished." 298 | ); 299 | } 300 | 301 | @Test 302 | public void cannot_be_put_after_the_test_is_finished() { 303 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 304 | executeTestWithRule( 305 | () -> {}, 306 | sftpServer 307 | ); 308 | Throwable exception = exceptionThrownBy( 309 | () -> sftpServer.putFile( 310 | "/dummy_file.txt", "dummy content", UTF_8 311 | ) 312 | ); 313 | assertThat(exception) 314 | .isInstanceOf(IllegalStateException.class) 315 | .hasMessage( 316 | "Failed to upload file because test has not been started" 317 | + " or is already finished." 318 | ); 319 | } 320 | } 321 | 322 | public static class a_binary_file { 323 | @Test 324 | public void that_is_put_to_root_directory_via_the_rule_can_be_read_from_server() { 325 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 326 | executeTestWithRule( 327 | () -> { 328 | sftpServer.putFile("/dummy_file.bin", DUMMY_CONTENT); 329 | byte[] file = downloadFile(sftpServer, "/dummy_file.bin"); 330 | assertThat(file).isEqualTo(DUMMY_CONTENT); 331 | }, 332 | sftpServer 333 | ); 334 | } 335 | 336 | @Test 337 | public void that_is_put_to_directory_via_the_rule_can_be_read_from_server() { 338 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 339 | executeTestWithRule( 340 | () -> { 341 | sftpServer.putFile( 342 | "/dummy_directory/dummy_file.bin", 343 | DUMMY_CONTENT 344 | ); 345 | byte[] file = downloadFile( 346 | sftpServer, 347 | "/dummy_directory/dummy_file.bin" 348 | ); 349 | assertThat(file).isEqualTo(DUMMY_CONTENT); 350 | }, 351 | sftpServer 352 | ); 353 | } 354 | 355 | @Test 356 | public void cannot_be_put_before_the_test_is_started() { 357 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 358 | Throwable exception = exceptionThrownBy( 359 | () -> sftpServer.putFile("/dummy_file.bin", DUMMY_CONTENT) 360 | ); 361 | assertThat(exception) 362 | .isInstanceOf(IllegalStateException.class) 363 | .hasMessage( 364 | "Failed to upload file because test has not been started" 365 | + " or is already finished." 366 | ); 367 | } 368 | 369 | @Test 370 | public void cannot_be_put_after_the_test_is_finished() { 371 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 372 | executeTestWithRule( 373 | () -> {}, 374 | sftpServer 375 | ); 376 | Throwable exception = exceptionThrownBy( 377 | () -> sftpServer.putFile("/dummy_file.bin", DUMMY_CONTENT) 378 | ); 379 | assertThat(exception) 380 | .isInstanceOf(IllegalStateException.class) 381 | .hasMessage( 382 | "Failed to upload file because test has not been started" 383 | + " or is already finished." 384 | ); 385 | } 386 | } 387 | 388 | public static class a_file_from_a_stream { 389 | @Test 390 | public void that_is_put_to_root_directory_via_the_rule_can_be_read_from_server() { 391 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 392 | executeTestWithRule( 393 | () -> { 394 | InputStream is = new ByteArrayInputStream(DUMMY_CONTENT); 395 | sftpServer.putFile("/dummy_file.bin", is); 396 | byte[] file = downloadFile(sftpServer, "/dummy_file.bin"); 397 | assertThat(file).isEqualTo(DUMMY_CONTENT); 398 | }, 399 | sftpServer 400 | ); 401 | } 402 | 403 | @Test 404 | public void that_is_put_to_directory_via_the_rule_can_be_read_from_server() { 405 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 406 | executeTestWithRule( 407 | () -> { 408 | InputStream is = new ByteArrayInputStream(DUMMY_CONTENT); 409 | sftpServer.putFile("/dummy_directory/dummy_file.bin", is); 410 | byte[] file = downloadFile( 411 | sftpServer, 412 | "/dummy_directory/dummy_file.bin" 413 | ); 414 | assertThat(file).isEqualTo(DUMMY_CONTENT); 415 | }, 416 | sftpServer 417 | ); 418 | } 419 | 420 | @Test 421 | public void cannot_be_put_before_the_test_is_started() { 422 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 423 | Throwable exception = exceptionThrownBy( 424 | () -> sftpServer.putFile("/dummy_file.bin", DUMMY_STREAM) 425 | ); 426 | assertThat(exception) 427 | .isInstanceOf(IllegalStateException.class) 428 | .hasMessage( 429 | "Failed to upload file because test has not been started" 430 | + " or is already finished." 431 | ); 432 | } 433 | 434 | @Test 435 | public void cannot_be_put_after_the_test_is_finished() { 436 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 437 | executeTestWithRule( 438 | () -> {}, 439 | sftpServer 440 | ); 441 | Throwable exception = exceptionThrownBy( 442 | () -> sftpServer.putFile("/dummy_file.bin", DUMMY_STREAM) 443 | ); 444 | assertThat(exception) 445 | .isInstanceOf(IllegalStateException.class) 446 | .hasMessage( 447 | "Failed to upload file because test has not been started" 448 | + " or is already finished." 449 | ); 450 | } 451 | } 452 | } 453 | 454 | @RunWith(Enclosed.class) 455 | public static class directory_creation { 456 | 457 | public static class a_single_directory { 458 | @Test 459 | public void that_is_created_with_the_rule_can_be_read_by_a_client() { 460 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 461 | executeTestWithRule( 462 | () -> { 463 | sftpServer.createDirectory("/a/directory"); 464 | assertEmptyDirectory(sftpServer, "/a/directory"); 465 | }, 466 | sftpServer 467 | ); 468 | } 469 | 470 | @Test 471 | public void cannot_be_created_before_the_test_is_started() { 472 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 473 | Throwable exception = exceptionThrownBy( 474 | () -> sftpServer.createDirectory("/a/directory") 475 | ); 476 | assertThat(exception) 477 | .isInstanceOf(IllegalStateException.class) 478 | .hasMessage( 479 | "Failed to create directory because test has not been" 480 | + " started or is already finished." 481 | ); 482 | } 483 | 484 | @Test 485 | public void cannot_be_created_after_the_test_is_finished() { 486 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 487 | executeTestWithRule( 488 | () -> {}, 489 | sftpServer 490 | ); 491 | Throwable exception = exceptionThrownBy( 492 | () -> sftpServer.createDirectory("/a/directory") 493 | ); 494 | assertThat(exception) 495 | .isInstanceOf(IllegalStateException.class) 496 | .hasMessage( 497 | "Failed to create directory because test has not been" 498 | + " started or is already finished." 499 | ); 500 | } 501 | } 502 | 503 | public static class multiple_directories { 504 | @Test 505 | public void that_are_created_with_the_rule_can_be_read_by_a_client() { 506 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 507 | executeTestWithRule( 508 | () -> { 509 | sftpServer.createDirectories( 510 | "/a/directory", 511 | "/another/directory" 512 | ); 513 | assertEmptyDirectory(sftpServer, "/a/directory"); 514 | assertEmptyDirectory(sftpServer, "/another/directory"); 515 | }, 516 | sftpServer 517 | ); 518 | } 519 | 520 | @Test 521 | public void cannot_be_created_before_the_test_is_started() { 522 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 523 | Throwable exception = exceptionThrownBy( 524 | () -> sftpServer.createDirectories( 525 | "/a/directory", 526 | "/another/directory" 527 | ) 528 | ); 529 | assertThat(exception) 530 | .isInstanceOf(IllegalStateException.class) 531 | .hasMessage( 532 | "Failed to create directory because test has not been" 533 | + " started or is already finished." 534 | ); 535 | } 536 | 537 | @Test 538 | public void cannot_be_created_after_the_test_is_finished() { 539 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 540 | executeTestWithRule( 541 | () -> {}, 542 | sftpServer 543 | ); 544 | Throwable exception = exceptionThrownBy( 545 | () -> sftpServer.createDirectories( 546 | "/a/directory", 547 | "/another/directory" 548 | ) 549 | ); 550 | assertThat(exception) 551 | .isInstanceOf(IllegalStateException.class) 552 | .hasMessage( 553 | "Failed to create directory because test has not been" 554 | + " started or is already finished." 555 | ); 556 | } 557 | } 558 | 559 | private static void assertEmptyDirectory( 560 | FakeSftpServerRule sftpServer, 561 | String directory 562 | ) throws JSchException, SftpException { 563 | Session session = connectToServer(sftpServer); 564 | ChannelSftp channel = connectSftpChannel(session); 565 | Vector entries = channel.ls(directory); 566 | assertThat(entries).hasSize(2); //these are the entries . and .. 567 | channel.disconnect(); 568 | session.disconnect(); 569 | } 570 | } 571 | 572 | @RunWith(Enclosed.class) 573 | public static class file_download { 574 | public static class a_text_file { 575 | @Test 576 | public void that_is_written_to_the_server_can_be_retrieved_with_the_rule() { 577 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 578 | executeTestWithRule( 579 | () -> { 580 | uploadFile( 581 | sftpServer, 582 | "/dummy_directory/dummy_file.txt", 583 | "dummy content with umlaut ü".getBytes(UTF_8) 584 | ); 585 | String fileContent = sftpServer.getFileContent( 586 | "/dummy_directory/dummy_file.txt", 587 | UTF_8 588 | ); 589 | assertThat(fileContent) 590 | .isEqualTo("dummy content with umlaut ü"); 591 | }, 592 | sftpServer 593 | ); 594 | } 595 | 596 | @Test 597 | public void cannot_be_retrieved_before_the_test_is_started() { 598 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 599 | Throwable exception = exceptionThrownBy( 600 | () -> sftpServer.getFileContent( 601 | "/dummy_directory/dummy_file.txt" 602 | ) 603 | ); 604 | assertThat(exception) 605 | .isInstanceOf(IllegalStateException.class) 606 | .hasMessage( 607 | "Failed to download file because test has not been started" 608 | + " or is already finished." 609 | ); 610 | } 611 | 612 | @Test 613 | public void cannot_be_retrieved_after_the_test_is_finished() { 614 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 615 | executeTestWithRule( 616 | () -> uploadFile( 617 | sftpServer, 618 | "/dummy_directory/dummy_file.txt", 619 | "dummy content".getBytes(UTF_8) 620 | ), 621 | sftpServer 622 | ); 623 | Throwable exception = exceptionThrownBy( 624 | () -> sftpServer.getFileContent( 625 | "/dummy_directory/dummy_file.txt" 626 | ) 627 | ); 628 | assertThat(exception) 629 | .isInstanceOf(IllegalStateException.class) 630 | .hasMessage( 631 | "Failed to download file because test has not been started" 632 | + " or is already finished." 633 | ); 634 | } 635 | } 636 | 637 | public static class a_binary_file { 638 | @Test 639 | public void that_is_written_to_the_server_can_be_retrieved_with_the_rule() { 640 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 641 | executeTestWithRule( 642 | () -> { 643 | uploadFile( 644 | sftpServer, 645 | "/dummy_directory/dummy_file.bin", 646 | DUMMY_CONTENT 647 | ); 648 | byte[] fileContent = sftpServer.getFileContent( 649 | "/dummy_directory/dummy_file.bin" 650 | ); 651 | assertThat(fileContent).isEqualTo(DUMMY_CONTENT); 652 | }, 653 | sftpServer 654 | ); 655 | } 656 | 657 | @Test 658 | public void cannot_be_retrieved_before_the_test_is_started() { 659 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 660 | Throwable exception = exceptionThrownBy( 661 | () -> sftpServer.getFileContent( 662 | "/dummy_directory/dummy_file.bin" 663 | ) 664 | ); 665 | assertThat(exception) 666 | .isInstanceOf(IllegalStateException.class) 667 | .hasMessage( 668 | "Failed to download file because test has not been started" 669 | + " or is already finished." 670 | ); 671 | } 672 | 673 | @Test 674 | public void cannot_be_retrieved_after_the_test_is_finished() { 675 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 676 | executeTestWithRule( 677 | () -> uploadFile( 678 | sftpServer, 679 | "/dummy_directory/dummy_file.bin", 680 | DUMMY_CONTENT 681 | ), 682 | sftpServer 683 | ); 684 | Throwable exception = exceptionThrownBy( 685 | () -> sftpServer.getFileContent("/dummy_file.bin") 686 | ); 687 | assertThat(exception) 688 | .isInstanceOf(IllegalStateException.class) 689 | .hasMessage( 690 | "Failed to download file because test has not been started" 691 | + " or is already finished." 692 | ); 693 | } 694 | } 695 | } 696 | 697 | public static class file_existence_check { 698 | 699 | @Test 700 | public void exists_returns_true_for_a_file_that_exists_on_the_server() { 701 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 702 | executeTestWithRule( 703 | () -> { 704 | uploadFile( 705 | sftpServer, 706 | "/dummy_directory/dummy_file.bin", 707 | DUMMY_CONTENT 708 | ); 709 | boolean exists = sftpServer.existsFile( 710 | "/dummy_directory/dummy_file.bin" 711 | ); 712 | assertThat(exists).isTrue(); 713 | }, 714 | sftpServer 715 | ); 716 | } 717 | 718 | @Test 719 | public void exists_returns_false_for_a_file_that_does_not_exists_on_the_server() { 720 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 721 | executeTestWithRule( 722 | () -> { 723 | boolean exists = sftpServer.existsFile( 724 | "/dummy_directory/dummy_file.bin" 725 | ); 726 | assertThat(exists).isFalse(); 727 | }, 728 | sftpServer 729 | ); 730 | } 731 | 732 | @Test 733 | public void exists_returns_false_for_a_directory_that_exists_on_the_server() { 734 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 735 | executeTestWithRule( 736 | () -> { 737 | uploadFile( 738 | sftpServer, 739 | "/dummy_directory/dummy_file.bin", 740 | DUMMY_CONTENT 741 | ); 742 | boolean exists = sftpServer.existsFile("/dummy_directory"); 743 | assertThat(exists).isFalse(); 744 | }, 745 | sftpServer 746 | ); 747 | } 748 | 749 | @Test 750 | public void existence_of_a_file_cannot_be_checked_before_the_test_is_started() { 751 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 752 | Throwable exception = exceptionThrownBy( 753 | () -> sftpServer.existsFile("/dummy_file.bin") 754 | ); 755 | assertThat(exception) 756 | .isInstanceOf(IllegalStateException.class) 757 | .hasMessage( 758 | "Failed to check existence of file because test has not" 759 | + " been started or is already finished." 760 | ); 761 | } 762 | 763 | @Test 764 | public void existence_of_a_file_cannot_be_checked_after_the_test_is_finished() { 765 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 766 | executeTestWithRule( 767 | () -> {}, 768 | sftpServer 769 | ); 770 | Throwable exception = exceptionThrownBy( 771 | () -> sftpServer.existsFile("/dummy_file.bin") 772 | ); 773 | assertThat(exception) 774 | .isInstanceOf(IllegalStateException.class) 775 | .hasMessage( 776 | "Failed to check existence of file because test has not" 777 | + " been started or is already finished." 778 | ); 779 | } 780 | } 781 | 782 | public static class server_shutdown { 783 | @Test 784 | public void after_a_successful_test_SFTP_server_is_shutdown() { 785 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 786 | .setPort(DUMMY_PORT); 787 | executeTestWithRule( 788 | () -> {}, 789 | sftpServer 790 | ); 791 | assertConnectionToSftpServerNotPossible(DUMMY_PORT); 792 | } 793 | 794 | @Test 795 | public void after_an_erroneous_test_SFTP_server_is_shutdown() { 796 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 797 | .setPort(DUMMY_PORT); 798 | executeTestThatThrowsExceptionWithRule( 799 | () -> { 800 | throw new RuntimeException(); 801 | }, 802 | sftpServer 803 | ); 804 | assertConnectionToSftpServerNotPossible(DUMMY_PORT); 805 | } 806 | 807 | @Test 808 | public void after_a_test_first_SFTP_server_is_shutdown_when_port_was_changed_during_test() { 809 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 810 | .setPort(DUMMY_PORT - 1); 811 | executeTestWithRule( 812 | () -> sftpServer.setPort(DUMMY_PORT), 813 | sftpServer 814 | ); 815 | assertConnectionToSftpServerNotPossible(DUMMY_PORT - 1); 816 | } 817 | 818 | @Test 819 | public void after_a_test_second_SFTP_server_is_shutdown_when_port_was_changed_during_test() { 820 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 821 | executeTestWithRule( 822 | () -> sftpServer.setPort(DUMMY_PORT), 823 | sftpServer 824 | ); 825 | assertConnectionToSftpServerNotPossible(DUMMY_PORT); 826 | } 827 | 828 | private void assertConnectionToSftpServerNotPossible( 829 | int port 830 | ) { 831 | Throwable throwable = catchThrowable( 832 | () -> connectToServerAtPort(port) 833 | ); 834 | assertThat(throwable) 835 | .withFailMessage( 836 | "SFTP server is still running on port %d.", 837 | port 838 | ) 839 | .hasCauseInstanceOf(ConnectException.class); 840 | } 841 | } 842 | 843 | public static class port_selection { 844 | @Test 845 | public void by_default_two_rules_run_servers_at_different_ports() { 846 | FakeSftpServerRule firstSftpServer = new FakeSftpServerRule(); 847 | AtomicInteger portCaptureForFirstServer = new AtomicInteger(); 848 | FakeSftpServerRule secondSftpServer = new FakeSftpServerRule(); 849 | AtomicInteger portCaptureForSecondServer = new AtomicInteger(); 850 | 851 | executeTestWithRule( 852 | () -> portCaptureForFirstServer.set(firstSftpServer.getPort()), 853 | firstSftpServer 854 | ); 855 | executeTestWithRule( 856 | () -> portCaptureForSecondServer.set(secondSftpServer.getPort()), 857 | secondSftpServer 858 | ); 859 | 860 | assertThat(portCaptureForFirstServer) 861 | .doesNotHaveValue(portCaptureForSecondServer.get()); 862 | } 863 | 864 | @Test 865 | public void the_port_can_be_changed_during_the_test() { 866 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 867 | executeTestWithRule( 868 | () -> { 869 | sftpServer.setPort(DUMMY_PORT); 870 | connectToServerAtPort(DUMMY_PORT); 871 | }, 872 | sftpServer 873 | ); 874 | } 875 | 876 | @Test 877 | public void it_is_not_possible_to_set_a_negative_port() { 878 | assertThatThrownBy(() -> new FakeSftpServerRule().setPort(-1)) 879 | .isInstanceOf(IllegalArgumentException.class) 880 | .hasMessage( 881 | "Port cannot be set to -1 because only ports between 1 and" 882 | + " 65535 are valid." 883 | ); 884 | } 885 | 886 | @Test 887 | public void it_is_not_possible_to_set_port_zero() { 888 | assertThatThrownBy(() -> new FakeSftpServerRule().setPort(0)) 889 | .isInstanceOf(IllegalArgumentException.class) 890 | .hasMessage( 891 | "Port cannot be set to 0 because only ports between 1 and" 892 | + " 65535 are valid." 893 | ); 894 | } 895 | 896 | @Test 897 | public void the_port_can_be_set_to_1() { 898 | //I don't test the connection, because port 1 can not be used by a 899 | //standard user. 900 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 901 | sftpServer.setPort(1); 902 | } 903 | 904 | @Test 905 | public void the_server_can_be_run_at_port_65535() { 906 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 907 | .setPort(65535); 908 | executeTestWithRule( 909 | () -> connectToServerAtPort(65535), 910 | sftpServer 911 | ); 912 | } 913 | 914 | @Test 915 | public void it_is_not_possible_to_set_a_port_greater_than_65535() { 916 | assertThatThrownBy(() -> new FakeSftpServerRule().setPort(65536)) 917 | .isInstanceOf(IllegalArgumentException.class) 918 | .hasMessage( 919 | "Port cannot be set to 65536 because only ports between 1" 920 | + " and 65535 are valid." 921 | ); 922 | } 923 | } 924 | 925 | @RunWith(Enclosed.class) 926 | public static class port_query { 927 | 928 | public static class random_port { 929 | @Test 930 | public void cannot_be_read_before_the_test() { 931 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 932 | assertPortCannotBeRead(sftpServer); 933 | } 934 | 935 | @Test 936 | public void can_be_read_during_the_test() { 937 | AtomicInteger portCapture = new AtomicInteger(); 938 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 939 | executeTestWithRule( 940 | () -> portCapture.set(sftpServer.getPort()), 941 | sftpServer 942 | ); 943 | assertThat(portCapture).doesNotHaveValue(0); 944 | } 945 | 946 | @Test 947 | public void cannot_be_read_after_the_test() { 948 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 949 | executeTestWithRule( 950 | () -> {}, 951 | sftpServer 952 | ); 953 | assertPortCannotBeRead(sftpServer); 954 | } 955 | 956 | private void assertPortCannotBeRead(FakeSftpServerRule sftpServer) { 957 | assertThatThrownBy(sftpServer::getPort) 958 | .isInstanceOf(IllegalStateException.class) 959 | .hasMessage( 960 | "Failed to call getPort() because test has not been" 961 | + " started or is already finished." 962 | ); 963 | } 964 | } 965 | 966 | public static class specified_port { 967 | @Test 968 | public void can_be_read_before_the_test() { 969 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 970 | .setPort(DUMMY_PORT); 971 | int port = sftpServer.getPort(); 972 | assertThat(port).isEqualTo(DUMMY_PORT); 973 | } 974 | 975 | @Test 976 | public void can_be_read_during_the_test() { 977 | AtomicInteger portCapture = new AtomicInteger(); 978 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 979 | .setPort(DUMMY_PORT); 980 | executeTestWithRule( 981 | () -> portCapture.set(sftpServer.getPort()), 982 | sftpServer 983 | ); 984 | assertThat(portCapture).hasValue(DUMMY_PORT); 985 | } 986 | 987 | @Test 988 | public void can_be_read_after_the_test() { 989 | FakeSftpServerRule sftpServer = new FakeSftpServerRule() 990 | .setPort(DUMMY_PORT); 991 | executeTestWithRule( 992 | () -> {}, 993 | sftpServer 994 | ); 995 | int port = sftpServer.getPort(); 996 | assertThat(port).isEqualTo(DUMMY_PORT); 997 | } 998 | } 999 | } 1000 | 1001 | public static class cleanup { 1002 | 1003 | @Test 1004 | public void deletes_file_in_root_directory() { 1005 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 1006 | executeTestWithRule( 1007 | () -> { 1008 | uploadFile( 1009 | sftpServer, 1010 | "/dummy_file.bin", 1011 | DUMMY_CONTENT 1012 | ); 1013 | sftpServer.deleteAllFilesAndDirectories(); 1014 | assertFileDoesNotExist( 1015 | sftpServer, 1016 | "/dummy_file.bin" 1017 | ); 1018 | }, 1019 | sftpServer 1020 | ); 1021 | } 1022 | 1023 | @Test 1024 | public void deletes_file_in_directory() { 1025 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 1026 | executeTestWithRule( 1027 | () -> { 1028 | uploadFile( 1029 | sftpServer, 1030 | "/dummy_directory/dummy_file.bin", 1031 | DUMMY_CONTENT 1032 | ); 1033 | sftpServer.deleteAllFilesAndDirectories(); 1034 | assertFileDoesNotExist( 1035 | sftpServer, 1036 | "/dummy_directory/dummy_file.bin" 1037 | ); 1038 | }, 1039 | sftpServer 1040 | ); 1041 | } 1042 | 1043 | @Test 1044 | public void deletes_directory() { 1045 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 1046 | executeTestWithRule( 1047 | () -> { 1048 | sftpServer.createDirectory("/dummy_directory"); 1049 | sftpServer.deleteAllFilesAndDirectories(); 1050 | assertDirectoryDoesNotExist( 1051 | sftpServer, 1052 | "/dummy_directory" 1053 | ); 1054 | }, 1055 | sftpServer 1056 | ); 1057 | } 1058 | 1059 | @Test 1060 | public void works_on_an_empty_filesystem() { 1061 | FakeSftpServerRule sftpServer = new FakeSftpServerRule(); 1062 | executeTestWithRule( 1063 | sftpServer::deleteAllFilesAndDirectories, 1064 | sftpServer 1065 | ); 1066 | } 1067 | 1068 | private static void assertFileDoesNotExist( 1069 | FakeSftpServerRule sftpServer, 1070 | String path 1071 | ) { 1072 | boolean exists = sftpServer.existsFile(path); 1073 | assertThat(exists).isFalse(); 1074 | } 1075 | 1076 | private static void assertDirectoryDoesNotExist( 1077 | FakeSftpServerRule sftpServer, 1078 | String directory 1079 | ) throws JSchException { 1080 | Session session = connectToServer(sftpServer); 1081 | ChannelSftp channel = connectSftpChannel(session); 1082 | try { 1083 | assertThatThrownBy(() -> channel.ls(directory)) 1084 | .isInstanceOf(SftpException.class) 1085 | .hasMessage("No such file or directory"); 1086 | } finally { 1087 | channel.disconnect(); 1088 | session.disconnect(); 1089 | } 1090 | } 1091 | } 1092 | 1093 | private static Session connectToServer( 1094 | FakeSftpServerRule sftpServer 1095 | ) throws JSchException { 1096 | return connectToServerAtPort(sftpServer.getPort()); 1097 | } 1098 | 1099 | private static Session connectToServerAtPort( 1100 | int port 1101 | ) throws JSchException { 1102 | Session session = createSessionWithCredentials( 1103 | "dummy user", "dummy password", port 1104 | ); 1105 | session.connect(TIMEOUT); 1106 | return session; 1107 | } 1108 | 1109 | private static ChannelSftp connectSftpChannel( 1110 | Session session 1111 | ) throws JSchException { 1112 | ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); 1113 | channel.connect(); 1114 | return channel; 1115 | } 1116 | 1117 | private static void connectAndDisconnect( 1118 | FakeSftpServerRule sftpServer 1119 | ) throws JSchException { 1120 | Session session = connectToServer(sftpServer); 1121 | ChannelSftp channel = connectSftpChannel(session); 1122 | channel.disconnect(); 1123 | session.disconnect(); 1124 | } 1125 | 1126 | private static Session createSessionWithCredentials( 1127 | String username, 1128 | String password, 1129 | int port 1130 | ) throws JSchException { 1131 | Session session = JSCH.getSession(username, "127.0.0.1", port); 1132 | session.setConfig("StrictHostKeyChecking", "no"); 1133 | session.setPassword(password); 1134 | return session; 1135 | } 1136 | 1137 | private static byte[] downloadFile( 1138 | FakeSftpServerRule server, 1139 | String path 1140 | ) throws JSchException, SftpException, IOException { 1141 | Session session = connectToServer(server); 1142 | ChannelSftp channel = connectSftpChannel(session); 1143 | try { 1144 | InputStream is = channel.get(path); 1145 | return toByteArray(is); 1146 | } finally { 1147 | channel.disconnect(); 1148 | session.disconnect(); 1149 | } 1150 | } 1151 | 1152 | private static void uploadFile( 1153 | FakeSftpServerRule server, 1154 | String pathAsString, 1155 | byte[] content 1156 | ) throws JSchException, SftpException { 1157 | Session session = connectToServer(server); 1158 | ChannelSftp channel = connectSftpChannel(session); 1159 | try { 1160 | Path path = Paths.get(pathAsString); 1161 | if (!path.getParent().equals(path.getRoot())) 1162 | channel.mkdir(path.getParent().toString()); 1163 | channel.put(new ByteArrayInputStream(content), pathAsString); 1164 | } finally { 1165 | channel.disconnect(); 1166 | session.disconnect(); 1167 | } 1168 | } 1169 | } 1170 | --------------------------------------------------------------------------------