This sample shows hot to setup proper FTP communication as client and server in Citrus. The ftp-client component creates a new directory on the server, stores a file and retrieves the same file in a single test.

FTP features are also described in detail in reference guide

Objectives

We want to setup both FTP client and server components in our project in order to test a file transfer via FTP. The client authenticates to the server using username password credentials. The ftp-server component will receive incoming requests for validation and provide the FTP user account workspace to the client.

First of all let us setup the necessary components in the Spring bean configuration:

@Bean
public FtpClient ftpClient() {
    return CitrusEndpoints.ftp()
            .client()
            .autoReadFiles(true)
            .port(22222)
            .username("citrus")
            .password("admin")
            .timeout(10000L)
            .build();
}

@Bean
public FtpServer ftpListServer() {
    return CitrusEndpoints.ftp()
            .server()
            .port(22222)
            .autoLogin(true)
            .autoStart(true)
            .autoHandleCommands(Stream.of(FTPCmd.MKD.getCommand(),
                                          FTPCmd.PORT.getCommand(),
                                          FTPCmd.TYPE.getCommand()).collect(Collectors.joining(",")))
            .userManagerProperties(new ClassPathResource("citrus.ftp.user.properties"))
            .build();
}

The ftpServer is a small but fully qualified FTP server implementation in Citrus. The server receives user-manager-properties that define all available user accounts. The properties look like this:

# Password is "admin"
ftpserver.user.citrus.userpassword=21232F297A57A5A743894A0E4A801FC3
ftpserver.user.citrus.homedirectory=target/ftp/user/citrus
ftpserver.user.citrus.enableflag=true
ftpserver.user.citrus.writepermission=true
ftpserver.user.citrus.maxloginnumber=0
ftpserver.user.citrus.maxloginperip=0
ftpserver.user.citrus.idletime=0
ftpserver.user.citrus.uploadrate=0
ftpserver.user.citrus.downloadrate=0

ftpserver.user.anonymous.userpassword=
ftpserver.user.anonymous.homedirectory=target/ftp/user/anonymous
ftpserver.user.anonymous.enableflag=true
ftpserver.user.anonymous.writepermission=false
ftpserver.user.anonymous.maxloginnumber=20
ftpserver.user.anonymous.maxloginperip=2
ftpserver.user.anonymous.idletime=300
ftpserver.user.anonymous.uploadrate=4800
ftpserver.user.anonymous.downloadrate=4800

The FTP server defines two accounts citrus and anonymous. Clients may authenticate to the server using these credentials. Based on the user account we can set a user workspace home directory. The server will save incoming stored files to this directory and the server will read retrieved files from that home directory.

In case you want to setup some files in that directory in order to provide it to clients, please copy those files to that home directory prior to the test.

The ftp-client connects to the server using the user credentials and is then able to store and retrieve files in a test.

In a sample test we first create a new subdirectory in that user home directory.

echo("Create new directory on server");

send(ftpClient)
    .fork(true)
    .message(FtpMessage.command(FTPCmd.MKD).arguments("todo"));

receive(ftpClient)
    .message(FtpMessage.result(getMkdirsCommandResult("todo")));

As you can see the client is passing a MKD signal to the server. The user login procedure is done automatically and the directory creation is also dome automatically on the FTP server. This is because we added the MKD signal to the list of auto-handle-commands.

@Bean
public FtpServer ftpListServer() {
    return CitrusEndpoints.ftp()
            .server()
            [...]
            .autoLogin(true)
            .autoHandleCommands(Stream.of(FTPCmd.MKD.getCommand(),
                                          FTPCmd.PORT.getCommand(),
                                          FTPCmd.TYPE.getCommand()).collect(Collectors.joining(",")))
            [...]
            .build();
}

This tells the FTP server to automatically handle the user login as well as the given commands. Now lets store a new file in that user directory.

echo("Store file to directory");

send(ftpClient)
        .fork(true)
        .message(FtpMessage.put("todo/entry.json", DataType.ASCII).arguments(""));

receive(ftpServer)
        .message(FtpMessage.command(FTPCmd.STOR).arguments("todo/entry.json"));

send(ftpServer)
        .message(FtpMessage.success());

receive(ftpClient)
        .message(FtpMessage.result(getStoreFileCommandResult()));

Now we have both client and server interaction in the same test case. This requires us to use fork=true option on all client requests as we need to continue with the test in order to handle the server interaction, too. We can store a new file todo/entry.json which is transmitted to the server using ASCII file mode.

The FTP server is receiving the STORsignal providing a success response in order to mark completion of the file transfer. After that the file should be created in the user home directory in path todo/entry.json. You can validate the file content by reading it from that directory in another test action.

Now we should be also able to list the files in that directory:

echo("List files in directory");

send(ftpClient)
        .fork(true)
        .message(FtpMessage.list("todo").arguments("todo"));

receive(ftpServer)
        .message(FtpMessage.command(FTPCmd.LIST).arguments("todo"));

send(ftpServer)
        .message(FtpMessage.success());

receive(ftpClient)
        .message(FtpMessage.result(getListCommandResult("entry.json")));
private ListCommandResult getListCommandResult(String ... fileNames) {
    ListCommandResult result = new ListCommandResult();
    result.setSuccess(true);
    result.setReplyCode(String.valueOf(226));
    result.setReplyString("@contains('Closing data connection')@");

    ListCommandResult.Files expectedFiles = new ListCommandResult.Files();

    for (String fileName : fileNames) {
        ListCommandResult.Files.File entry = new ListCommandResult.Files.File();
        entry.setPath(fileName);
        expectedFiles.getFiles().add(entry);
    }

    result.setFiles(expectedFiles);

    return result;
}

Now we can also retrieve the file from the server by calling the RETR operation.

echo("Retrieve file from server");

send(ftpClient)
        .fork(true)
        .message(FtpMessage.get("todo/todo.json", "target/todo/todo.json", DataType.ASCII));

receive(ftpServer)
        .message(FtpMessage.command(FTPCmd.RETR).arguments("todo/todo.json"));

send(ftpServer)
        .message(FtpMessage.success());

receive(ftpClient)
        .message(FtpMessage.result(getRetrieveFileCommandResult("target/todo/todo.json", new ClassPathResource("todo/entry.json"))));
private GetCommandResult getRetrieveFileCommandResult(String path, Resource content) throws IOException {
    GetCommandResult result = new GetCommandResult();
    result.setSuccess(true);
    result.setReplyCode(String.valueOf(226));
    result.setReplyString("@contains('Transfer complete')@");

    GetCommandResult.File entryResult = new GetCommandResult.File();
    entryResult.setPath(path);
    entryResult.setData(FileUtils.readToString(content));
    result.setFile(entryResult);

    return result;
}

This completes our test as we were able to interact with the FTP server using the client signals.

Run

You can execute some sample Citrus test cases in this sample in order to write the reports. Open a separate command line terminal and navigate to the sample folder.

Execute all Citrus tests by calling

 mvn integration-test

You should see Citrus performing several tests with lots of debugging output. And of course green tests at the very end of the build and some new reporting files in target/citrus-reports folder.

Of course you can also start the Citrus tests from your favorite IDE. Just start the Citrus test using the TestNG IDE integration in IntelliJ, Eclipse or Netbeans.