FakFtpServerIntegrationTest (first phase); homeDirectory support
git-svn-id: svn://svn.code.sf.net/p/mockftpserver/code@64 531de8e6-9941-0410-b38b-9a92acbe0330
diff --git a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandler.groovy b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandler.groovy
index 99aa693..322b614 100644
--- a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandler.groovy
+++ b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandler.groovy
@@ -32,6 +32,8 @@
import org.mockftpserver.fake.filesystem.FileSystemException
import org.mockftpserver.fake.filesystem.InvalidFilenameException
import org.mockftpserver.fake.filesystem.NewFileOperationException
+import org.mockftpserver.fake.user.UserAccount
+
/**
* Abstract superclass for CommandHandler classes for the "Fake" server.
@@ -46,7 +48,7 @@
ServerConfiguration serverConfiguration
/**
- * Reply code sent back when a FileSystemException is caught by the {@link #handleCommand(Command, Session)}
+ * Reply code sent back when a FileSystemException is caught by the {@link #handleCommand(Command, Session)}
* This defaults to ReplyCodes.EXISTING_FILE_ERROR (550).
*/
int replyCodeForFileSystemException = ReplyCodes.EXISTING_FILE_ERROR
@@ -105,6 +107,37 @@
* Send a reply for this command on the control connection.
*
* The reply code is designated by the <code>replyCode</code> property, and the reply text
+ * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey.
+ *
+ * @param session - the Session
+ * @param replyCode - the reply code
+ * @param messageKey - the resource bundle key for the reply text
+ * @param args - the optional message arguments; defaults to []
+ *
+ * @throws AssertionError - if session is null
+ *
+ * @see MessageFormat
+ */
+ protected void sendReply(Session session, int replyCode, String messageKey, List args = []) {
+ assert session
+ assertValidReplyCode(replyCode);
+
+ String key = Integer.toString(replyCode);
+ String text = getTextForKey(messageKey)
+
+ String replyText = (args) ? MessageFormat.format(text, args as Object[]) : text;
+
+ String replyTextToLog = (replyText == null) ? "" : " " + replyText;
+ // TODO change to LOG.debug()
+ def argsToLog = (args) ? " args=$args" : ""
+ LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog);
+ session.sendReply(replyCode, replyText);
+ }
+
+ /**
+ * Send a reply for this command on the control connection.
+ *
+ * The reply code is designated by the <code>replyCode</code> property, and the reply text
* is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key.
*
* @param session - the Session
@@ -115,20 +148,8 @@
*
* @see MessageFormat
*/
- protected void sendReply(Session session, int replyCode, args = []) {
- assert session
- assertValidReplyCode(replyCode);
-
- String key = Integer.toString(replyCode);
- String text = getTextForReplyCode(replyCode)
-
- String replyText = (args) ? MessageFormat.format(text, args as Object[]) : text;
-
- String replyTextToLog = (replyText == null) ? "" : " " + replyText;
- // TODO change to LOG.debug()
- def argsToLog = (args) ? " args=$args" : ""
- LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog);
- session.sendReply(replyCode, replyText);
+ protected void sendReply(Session session, int replyCode, List args = []) {
+ sendReply(session, replyCode, replyCode.toString(), args)
}
/**
@@ -238,15 +259,58 @@
"\n"
}
- private String getTextForReplyCode(int replyCode) {
+ private String getTextForKey(key) {
try {
- return serverConfiguration.replyTextBundle.getString(Integer.toString(replyCode))
+ return serverConfiguration.replyTextBundle.getString(key.toString())
}
catch (MissingResourceException e) {
// No reply text is mapped for the specified key
- LOG.warn("No reply text defined for reply code [${replyCode as String}]");
+ LOG.warn("No reply text defined for key [${key.toString()}]");
return null;
}
}
+ // -------------------------------------------------------------------------
+ // Login Support (used by USER and PASS commands)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does
+ * not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate
+ * error message, and return false. A UserAccount is considered invalid if the homeDirectory property
+ * is not set or is set to a non-existent directory.
+ * @param username - the username
+ * @param session - the session; used to send back an error reply if necessary
+ * @return true only if the UserAccount for the named user is valid
+ */
+ protected boolean validateUserAccount(String username, Session session) {
+ def userAccount = serverConfiguration.getUserAccount(username)
+ if (userAccount == null || !userAccount.valid) {
+ LOG.error("UserAccount missing or not valid for username [$username]: $userAccount")
+ sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "userAccountNotValid", [username])
+ return false
+ }
+
+ def home = userAccount.homeDirectory
+ if (!getFileSystem().isDirectory(home)) {
+ LOG.error("Home directory configured for username [$username] is not valid: $home")
+ sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "homeDirectoryNotValid", [username, home])
+ return false
+ }
+
+ return true
+ }
+
+ /**
+ * Log in the specified user for the current session. Send back a reply of 230 and set the UserAccount
+ * and current directory (homeDirectory) in the session
+ * @param userAccount - the userAccount for the user to be logged in
+ * @param session - the session
+ */
+ protected void login(UserAccount userAccount, Session session) {
+ sendReply(session, ReplyCodes.PASS_OK)
+ session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount)
+ session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.homeDirectory)
+ }
+
}
\ No newline at end of file
diff --git a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/PassCommandHandler.groovy b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/PassCommandHandler.groovy
index 320e4d0..0d6f60a 100644
--- a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/PassCommandHandler.groovy
+++ b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/PassCommandHandler.groovy
Binary files differ
diff --git a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/UserCommandHandler.groovy b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/UserCommandHandler.groovy
index c34fd7a..b91c7f6 100644
--- a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/UserCommandHandler.groovy
+++ b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/command/UserCommandHandler.groovy
Binary files differ
diff --git a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/user/UserAccount.groovy b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/user/UserAccount.groovy
index 09c71b9..625a9cd 100644
--- a/MockFtpServer/src/main/groovy/org/mockftpserver/fake/user/UserAccount.groovy
+++ b/MockFtpServer/src/main/groovy/org/mockftpserver/fake/user/UserAccount.groovy
@@ -30,22 +30,22 @@
* value is ignored, and the <code>isValidPassword()</code> method just returns <code<true</code>.
*/
class UserAccount {
-
+
String username
String password
String homeDirectory
boolean passwordRequiredForLogin = true
boolean passwordCheckedDuringValidation = true
-
+
/**
* Return true if the specified password is the correct, valid password for this user account.
* This implementation uses standard (case-sensitive) String comparison. Subclasses can provide
* custom comparison behavior, for instance using encrypted password values, by overriding this
* method.
- *
+ *
* @param password - the password to compare against the configured value
* @return true if the password is correct and valid
- *
+ *
* @throws AssertionError - if the username property is null
*/
boolean isValidPassword(String password) {
@@ -54,19 +54,26 @@
}
/**
+ * @return true if this UserAccount object is valid; i.e. if the homeDirectory is non-null and non-empty.
+ */
+ boolean isValid() {
+ return homeDirectory
+ }
+
+ /**
* @return the String representation of this object
*/
String toString() {
"UserAccount[username=$username; password=$password; homeDirectory=$homeDirectory; " +
- "passwordRequiredForLogin=$passwordRequiredForLogin]"
+ "passwordRequiredForLogin=$passwordRequiredForLogin]"
}
-
+
/**
* Return true if the specified password matches the password configured for this user account.
* This implementation uses standard (case-sensitive) String comparison. Subclasses can provide
* custom comparison behavior, for instance using encrypted password values, by overriding this
* method.
- *
+ *
* @param password - the password to compare against the configured value
* @return true if the passwords match
*/
diff --git a/MockFtpServer/src/main/java/org/mockftpserver/core/command/ReplyCodes.java b/MockFtpServer/src/main/java/org/mockftpserver/core/command/ReplyCodes.java
index c41f519..449181b 100644
--- a/MockFtpServer/src/main/java/org/mockftpserver/core/command/ReplyCodes.java
+++ b/MockFtpServer/src/main/java/org/mockftpserver/core/command/ReplyCodes.java
@@ -16,11 +16,10 @@
package org.mockftpserver.core.command;
/**
- * Reply Code constants.
- *
- * @version $Revision$ - $Date$
+ * Reply Code constants.
*
* @author Chris Mair
+ * @version $Revision$ - $Date$
*/
public final class ReplyCodes {
@@ -55,7 +54,8 @@
public static final int USER_LOGGED_IN_OK = 230;
public static final int USER_NEED_PASSWORD_OK = 331;
public static final int USER_NO_SUCH_USER = 530;
-
+ public static final int USER_ACCOUNT_NOT_VALID = 530;
+
public static final int SEND_DATA_INITIAL_OK = 150;
public static final int SEND_DATA_FINAL_OK = 226;
@@ -69,8 +69,8 @@
public static final int EXISTING_FILE_ERROR = 550;
public static final int NEW_FILE_ERROR = 553;
public static final int FILENAME_NOT_VALID = 553;
-
-
+
+
/**
* Private constructor. This class should not be instantiated.
*/
diff --git a/MockFtpServer/src/main/resources/ReplyText.properties b/MockFtpServer/src/main/resources/ReplyText.properties
index 37f6033..87e017a 100644
--- a/MockFtpServer/src/main/resources/ReplyText.properties
+++ b/MockFtpServer/src/main/resources/ReplyText.properties
@@ -60,3 +60,9 @@
# Exceeded storage allocation (for current directory or dataset).
553=Requested action not taken for {0}
# File name not allowed.
+
+#-------------------------------------------------------------------------------
+# Other Messages
+#-------------------------------------------------------------------------------
+userAccountNotValid=UserAccount missing or invalid for user [{0}]
+homeDirectoryNotValid=The homeDirectory configured for user [{0}] is not a valid directory: [{1}]
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy
index 99faa24..d858e44 100644
--- a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy
@@ -20,7 +20,7 @@
import org.mockftpserver.fake.user.UserAccount
/**
- * Stub implementation of the {@link ServerConfiguration} interface for testing
+ * Stub implementation of the {@link ServerConfiguration} interface for testing
*
* @version $Revision$ - $Date$
*
@@ -46,12 +46,13 @@
//-------------------------------------------------------------------------
/**
- * Set the text to be returned for the specified reply code by the
- * {@link #getReplyTextBundle()} method.
+ * Set the text to be returned for the specified key by the
+ * {@link #getReplyTextBundle()} resource bundle.
*/
- void setTextForReplyCode(int replyCode, String text) {
- textForReplyCodeMap[replyCode as String] = text
+ void setTextForKey(key, String text) {
+ textForReplyCodeMap[key.toString()] = text
}
+
}
/**
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy
index f0a30a6..e02aff2 100644
--- a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy
Binary files differ
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy
index 296828b..cf6c1a4 100644
--- a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy
Binary files differ
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy
index 3a19bc1..86f3efb 100644
--- a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy
Binary files differ
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy
index 8a1cfad..1b7a49d 100644
--- a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy
@@ -42,9 +42,11 @@
static PATH = "some/path"
static REPLY_CODE = 99
+ static MESSAGE_KEY = "99.WithFilename"
static ARG = "ABC"
static MSG = "text {0}"
static MSG_WITH_ARG = "text ABC"
+ static MSG_FOR_KEY = "some other message"
private AbstractFakeCommandHandler commandHandler
private session
private serverConfiguration
@@ -87,6 +89,14 @@
shouldFail { commandHandler.sendReply(session, 0) }
}
+ void testSendReply_MessageKey() {
+ commandHandler.sendReply(session, REPLY_CODE, MESSAGE_KEY)
+ assert session.sentReplies[0] == [REPLY_CODE, MSG_FOR_KEY], session.sentReplies[0]
+
+ shouldFail { commandHandler.sendReply(null, REPLY_CODE, MESSAGE_KEY) }
+ shouldFail { commandHandler.sendReply(session, 0, MESSAGE_KEY) }
+ }
+
void testAssertValidReplyCode() {
commandHandler.assertValidReplyCode(1) // no exception expected
shouldFail { commandHandler.assertValidReplyCode(0) }
@@ -145,7 +155,8 @@
fileSystem = new FakeUnixFileSystem()
serverConfiguration.setFileSystem(fileSystem)
- serverConfiguration.setTextForReplyCode(REPLY_CODE, MSG)
+ serverConfiguration.setTextForKey(REPLY_CODE, MSG)
+ serverConfiguration.setTextForKey(MESSAGE_KEY, MSG_FOR_KEY)
commandHandler.serverConfiguration = serverConfiguration
}
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/server/FakeFtpServerIntegrationTest.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/server/FakeFtpServerIntegrationTest.groovy
new file mode 100644
index 0000000..7ae2b13
--- /dev/null
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/server/FakeFtpServerIntegrationTest.groovy
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mockftpserver.fake.server
+
+import org.apache.commons.net.ftp.FTPClient
+import org.mockftpserver.fake.filesystem.FakeWindowsFileSystem
+import org.mockftpserver.fake.filesystem.FileSystem
+import org.mockftpserver.fake.user.UserAccount
+import org.mockftpserver.test.AbstractGroovyTest
+import org.mockftpserver.test.PortTestUtil
+
+/**
+ * Integration tests for FakeFtpServer.
+ *
+ * @version $Revision: 54 $ - $Date: 2008-05-13 21:54:53 -0400 (Tue, 13 May 2008) $
+ *
+ * @author Chris Mair
+ */
+class FakeFtpServerIntegrationTest extends AbstractGroovyTest {
+
+ static final SERVER = "localhost";
+ static final USERNAME = "user123";
+ static final PASSWORD = "password";
+ static final FILENAME = "abc.txt";
+ static final ASCII_CONTENTS = "abcdef\tghijklmnopqr";
+ static final BINARY_CONTENTS = new byte[256];
+ static final HOME_DIRECTORY = "/"
+
+ private FakeFtpServer ftpServer;
+ private FTPClient ftpClient;
+ private FileSystem fileSystem
+
+ //-------------------------------------------------------------------------
+ // Tests
+ //-------------------------------------------------------------------------
+
+ void testLogin() {
+ // Connect
+ LOG.info("Conecting to " + SERVER)
+ ftpClientConnect()
+ verifyReplyCode("connect", 220)
+
+ // Login
+ String userAndPassword = USERNAME + "/" + PASSWORD
+ LOG.info("Logging in as " + userAndPassword)
+ boolean success = ftpClient.login(USERNAME, PASSWORD)
+ assertTrue("Unable to login with " + userAndPassword, success)
+ verifyReplyCode("login with " + userAndPassword, 230)
+
+ // Quit
+ ftpClient.quit()
+ verifyReplyCode("quit", 221)
+ }
+
+// void testPwd() {
+// ftpClientConnect()
+//
+// String dir = ftpClient.printWorkingDirectory()
+// assert dir == DIR
+// verifyReplyCode("printWorkingDirectory", 257)
+// }
+
+ // -------------------------------------------------------------------------
+ // Test setup and tear-down
+ // -------------------------------------------------------------------------
+
+ /**
+ * Perform initialization before each test
+ * @see org.mockftpserver.test.AbstractTest#setUp()
+ */
+ void setUp() {
+ super.setUp();
+
+ for (int i = 0; i < BINARY_CONTENTS.length; i++) {
+ BINARY_CONTENTS[i] = (byte) i;
+ }
+
+ ftpServer = new FakeFtpServer();
+ ftpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort());
+
+ fileSystem = new FakeWindowsFileSystem()
+ ftpServer.fileSystem = fileSystem
+ fileSystem.createDirectory(HOME_DIRECTORY)
+
+ def userAccount = new UserAccount(username: USERNAME, password: PASSWORD,
+ passwordRequiredForLogin: true, homeDirectory: HOME_DIRECTORY)
+ ftpServer.userAccounts[USERNAME] = userAccount
+
+ ftpServer.start();
+ ftpClient = new FTPClient();
+ }
+
+ /**
+ * Perform cleanup after each test
+ * @see org.mockftpserver.test.AbstractTest#tearDown()
+ */
+ void tearDown() {
+ super.tearDown();
+ ftpServer.stop();
+ }
+
+ // -------------------------------------------------------------------------
+ // Internal Helper Methods
+ // -------------------------------------------------------------------------
+
+ /**
+ * Connect to the server from the FTPClient
+ */
+ private void ftpClientConnect() throws IOException {
+ ftpClient.connect(SERVER, PortTestUtil.getFtpServerControlPort());
+ }
+
+ /**
+ * Assert that the FtpClient reply code is equal to the expected value
+ *
+ * @param operation - the description of the operation performed; used in the error message
+ * @param expectedReplyCode - the expected FtpClient reply code
+ */
+ private void verifyReplyCode(String operation, int expectedReplyCode) {
+ int replyCode = ftpClient.getReplyCode();
+ LOG.info("Reply: operation=\"" + operation + "\" replyCode=" + replyCode);
+ assertEquals("Unexpected replyCode for " + operation, expectedReplyCode, replyCode);
+ }
+
+}
\ No newline at end of file
diff --git a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/user/UserAccountTest.groovy b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/user/UserAccountTest.groovy
index 19ca6bb..5e69008 100644
--- a/MockFtpServer/src/test/groovy/org/mockftpserver/fake/user/UserAccountTest.groovy
+++ b/MockFtpServer/src/test/groovy/org/mockftpserver/fake/user/UserAccountTest.groovy
@@ -15,11 +15,11 @@
*/
package org.mockftpserver.fake.user
-import org.mockftpserver.test.*
+import org.mockftpserver.test.AbstractGroovyTest
/**
* Tests for UserAccount
- *
+ *
* @version $Revision$ - $Date$
*
* @author Chris Mair
@@ -31,25 +31,25 @@
private static final HOME_DIR = "/usr/user123"
private UserAccount userAccount
-
+
void testIsValidPassword() {
userAccount.username = USERNAME
userAccount.password = PASSWORD
assert userAccount.isValidPassword(PASSWORD)
-
+
assert userAccount.isValidPassword("") == false
assert userAccount.isValidPassword("wrong") == false
assert userAccount.isValidPassword(null) == false
}
-
+
void testIsValidPassword_UsernameNullOrEmpty() {
userAccount.password = PASSWORD
shouldFail(AssertionError) { userAccount.isValidPassword(PASSWORD) }
userAccount.username = ''
shouldFail(AssertionError) { userAccount.isValidPassword(PASSWORD) }
- }
-
+ }
+
void testIsValidPassword_OverrideComparePassword() {
def customUserAccount = new CustomUserAccount()
customUserAccount.username = USERNAME
@@ -58,14 +58,22 @@
assert customUserAccount.isValidPassword(PASSWORD) == false
assert customUserAccount.isValidPassword(PASSWORD + "123")
}
-
+
void testIsValidPassword_PasswordNotCheckedDuringValidation() {
userAccount.username = USERNAME
userAccount.password = PASSWORD
userAccount.passwordCheckedDuringValidation = false
assert userAccount.isValidPassword("wrong")
}
-
+
+ void testIsValid() {
+ assert !userAccount.valid
+ userAccount.homeDirectory = ""
+ assert !userAccount.valid
+ userAccount.homeDirectory = "/abc"
+ assert userAccount.valid
+ }
+
void setUp() {
userAccount = new UserAccount()
}