1/*
2 * Copyright 2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.mockftpserver.stub;
17
18import org.apache.commons.net.ftp.FTP;
19import org.apache.commons.net.ftp.FTPClient;
20import org.apache.commons.net.ftp.FTPFile;
21import org.apache.log4j.Logger;
22import org.mockftpserver.core.command.CommandHandler;
23import org.mockftpserver.core.command.CommandNames;
24import org.mockftpserver.core.command.InvocationRecord;
25import org.mockftpserver.core.command.SimpleCompositeCommandHandler;
26import org.mockftpserver.core.command.StaticReplyCommandHandler;
27import org.mockftpserver.stub.command.*;
28import org.mockftpserver.test.*;
29import org.mockftpserver.test.AbstractTestCase;
30
31import java.io.ByteArrayInputStream;
32import java.io.ByteArrayOutputStream;
33import java.io.IOException;
34
35/**
36 * Tests for StubFtpServer using the Apache Jakarta Commons Net FTP client.
37 *
38 * @author Chris Mair
39 * @version $Revision$ - $Date$
40 */
41public final class StubFtpServerIntegrationTest extends AbstractTestCase implements IntegrationTest {
42
43    private static final Logger LOG = Logger.getLogger(StubFtpServerIntegrationTest.class);
44    private static final String SERVER = "localhost";
45    private static final String USERNAME = "user123";
46    private static final String PASSWORD = "password";
47    private static final String FILENAME = "abc.txt";
48    private static final String ASCII_CONTENTS = "abcdef\tghijklmnopqr";
49    private static final byte[] BINARY_CONTENTS = new byte[256];
50
51    private StubFtpServer stubFtpServer;
52    private FTPClient ftpClient;
53    private RetrCommandHandler retrCommandHandler;
54    private StorCommandHandler storCommandHandler;
55
56    //-------------------------------------------------------------------------
57    // Tests
58    //-------------------------------------------------------------------------
59
60    public void testLogin() throws Exception {
61        // Connect
62        LOG.info("Conecting to " + SERVER);
63        ftpClientConnect();
64        verifyReplyCode("connect", 220);
65
66        // Login
67        String userAndPassword = USERNAME + "/" + PASSWORD;
68        LOG.info("Logging in as " + userAndPassword);
69        boolean success = ftpClient.login(USERNAME, PASSWORD);
70        assertTrue("Unable to login with " + userAndPassword, success);
71        verifyReplyCode("login with " + userAndPassword, 230);
72
73        assertTrue("isStarted", stubFtpServer.isStarted());
74        assertFalse("isShutdown", stubFtpServer.isShutdown());
75
76        // Quit
77        LOG.info("Quit");
78        ftpClient.quit();
79        verifyReplyCode("quit", 221);
80    }
81
82    public void testAcct() throws Exception {
83        ftpClientConnect();
84
85        // ACCT
86        int replyCode = ftpClient.acct("123456");
87        assertEquals("acct", 230, replyCode);
88    }
89
90    /**
91     * Test the stop() method when no session has ever been started
92     */
93    public void testStop_NoSessionEverStarted() throws Exception {
94        LOG.info("Testing a stop() when no session has ever been started");
95    }
96
97    public void testHelp() throws Exception {
98        // Modify HELP CommandHandler to return a predefined help message
99        final String HELP = "help message";
100        HelpCommandHandler helpCommandHandler = (HelpCommandHandler) stubFtpServer.getCommandHandler(CommandNames.HELP);
101        helpCommandHandler.setHelpMessage(HELP);
102
103        ftpClientConnect();
104
105        // HELP
106        String help = ftpClient.listHelp();
107        assertTrue("Wrong response", help.indexOf(HELP) != -1);
108        verifyReplyCode("listHelp", 214);
109    }
110
111    /**
112     * Test the LIST and SYST commands.
113     */
114    public void testList() throws Exception {
115        ftpClientConnect();
116
117        // Set directory listing
118        ListCommandHandler listCommandHandler = (ListCommandHandler) stubFtpServer.getCommandHandler(CommandNames.LIST);
119        listCommandHandler.setDirectoryListing("11-09-01 12:30PM  406348 File2350.log\n"
120                + "11-01-01 1:30PM <DIR> 0 archive");
121
122        // LIST
123        FTPFile[] files = ftpClient.listFiles();
124        assertEquals("number of files", 2, files.length);
125        verifyFTPFile(files[0], FTPFile.FILE_TYPE, "File2350.log", 406348L);
126        verifyFTPFile(files[1], FTPFile.DIRECTORY_TYPE, "archive", 0L);
127        verifyReplyCode("list", 226);
128    }
129
130    /**
131     * Test the LIST, PASV and SYST commands, transferring a directory listing in passive mode
132     */
133    public void testList_PassiveMode() throws Exception {
134        ftpClientConnect();
135
136        ftpClient.enterLocalPassiveMode();
137
138        // Set directory listing
139        ListCommandHandler listCommandHandler = (ListCommandHandler) stubFtpServer.getCommandHandler(CommandNames.LIST);
140        listCommandHandler.setDirectoryListing("11-09-01 12:30PM  406348 File2350.log");
141
142        // LIST
143        FTPFile[] files = ftpClient.listFiles();
144        assertEquals("number of files", 1, files.length);
145        verifyReplyCode("list", 226);
146    }
147
148    public void testNlst() throws Exception {
149        ftpClientConnect();
150
151        // Set directory listing
152        NlstCommandHandler nlstCommandHandler = (NlstCommandHandler) stubFtpServer.getCommandHandler(CommandNames.NLST);
153        nlstCommandHandler.setDirectoryListing("File1.txt\nfile2.data");
154
155        // NLST
156        String[] filenames = ftpClient.listNames();
157        assertEquals("number of files", 2, filenames.length);
158        assertEquals(filenames[0], "File1.txt");
159        assertEquals(filenames[1], "file2.data");
160        verifyReplyCode("listNames", 226);
161    }
162
163    /**
164     * Test printing the current working directory (PWD)
165     */
166    public void testPwd() throws Exception {
167        // Modify PWD CommandHandler to return a predefined directory
168        final String DIR = "some/dir";
169        PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler(CommandNames.PWD);
170        pwdCommandHandler.setDirectory(DIR);
171
172        ftpClientConnect();
173
174        // PWD
175        String dir = ftpClient.printWorkingDirectory();
176        assertEquals("Unable to PWD", DIR, dir);
177        verifyReplyCode("printWorkingDirectory", 257);
178    }
179
180    public void testStat() throws Exception {
181        // Modify Stat CommandHandler to return predefined text
182        final String STATUS = "some information 123";
183        StatCommandHandler statCommandHandler = (StatCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STAT);
184        statCommandHandler.setStatus(STATUS);
185
186        ftpClientConnect();
187
188        // STAT
189        String status = ftpClient.getStatus();
190        assertEquals("STAT reply", "211 " + STATUS + ".", status.trim());
191        verifyReplyCode("getStatus", 211);
192    }
193
194    /**
195     * Test getting the status (STAT), when the reply text contains multiple lines
196     */
197    public void testStat_MultilineReplyText() throws Exception {
198        // Modify Stat CommandHandler to return predefined text
199        final String STATUS = "System name: abc.def\nVersion 3.5.7\nNumber of failed logins: 2";
200        final String FORMATTED_REPLY_STATUS = "211-System name: abc.def\r\nVersion 3.5.7\r\n211 Number of failed logins: 2.";
201        StatCommandHandler statCommandHandler = (StatCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STAT);
202        statCommandHandler.setStatus(STATUS);
203
204        ftpClientConnect();
205
206        // STAT
207        String status = ftpClient.getStatus();
208        assertEquals("STAT reply", FORMATTED_REPLY_STATUS, status.trim());
209        verifyReplyCode("getStatus", 211);
210    }
211
212    public void testSyst() throws Exception {
213        ftpClientConnect();
214
215        // SYST
216        assertEquals("getSystemName()", "\"WINDOWS\" system type.", ftpClient.getSystemName());
217        verifyReplyCode("syst", 215);
218    }
219
220    public void testCwd() throws Exception {
221        // Connect
222        LOG.info("Conecting to " + SERVER);
223        ftpClientConnect();
224        verifyReplyCode("connect", 220);
225
226        // CWD
227        boolean success = ftpClient.changeWorkingDirectory("dir1/dir2");
228        assertTrue("Unable to CWD", success);
229        verifyReplyCode("changeWorkingDirectory", 250);
230    }
231
232    /**
233     * Test changing the current working directory (CWD), when it causes a remote error
234     */
235    public void testCwd_Error() throws Exception {
236        // Override CWD CommandHandler to return error reply code
237        final int REPLY_CODE = 500;
238        StaticReplyCommandHandler cwdCommandHandler = new StaticReplyCommandHandler(REPLY_CODE);
239        stubFtpServer.setCommandHandler("CWD", cwdCommandHandler);
240
241        ftpClientConnect();
242
243        // CWD
244        boolean success = ftpClient.changeWorkingDirectory("dir1/dir2");
245        assertFalse("Expected failure", success);
246        verifyReplyCode("changeWorkingDirectory", REPLY_CODE);
247    }
248
249    public void testCdup() throws Exception {
250        ftpClientConnect();
251
252        // CDUP
253        boolean success = ftpClient.changeToParentDirectory();
254        assertTrue("Unable to CDUP", success);
255        verifyReplyCode("changeToParentDirectory", 200);
256    }
257
258    public void testDele() throws Exception {
259        ftpClientConnect();
260
261        // DELE
262        boolean success = ftpClient.deleteFile(FILENAME);
263        assertTrue("Unable to DELE", success);
264        verifyReplyCode("deleteFile", 250);
265    }
266
267    public void testEprt() throws Exception {
268        ftpClientConnect();
269        ftpClient.sendCommand("EPRT", "|2|1080::8:800:200C:417A|5282|");
270        verifyReplyCode("EPRT", 200);
271    }
272
273    public void testEpsv() throws Exception {
274        ftpClientConnect();
275        ftpClient.sendCommand("EPSV");
276        verifyReplyCode("EPSV", 229);
277    }
278
279    public void testFeat_UseStaticReplyCommandHandler() throws IOException {
280        // The FEAT command is not supported out of the box
281        final String FEAT_TEXT = "Extensions supported:\n" +
282                "MLST size*;create;modify*;perm;media-type\n" +
283                "SIZE\n" +
284                "COMPRESSION\n" +
285                "END";
286        StaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, FEAT_TEXT);
287        stubFtpServer.setCommandHandler("FEAT", featCommandHandler);
288
289        ftpClientConnect();
290        assertEquals(ftpClient.sendCommand("FEAT"), 211);
291        LOG.info(ftpClient.getReplyString());
292    }
293
294    public void testMkd() throws Exception {
295        ftpClientConnect();
296
297        // MKD
298        boolean success = ftpClient.makeDirectory("dir1/dir2");
299        assertTrue("Unable to CWD", success);
300        verifyReplyCode("makeDirectory", 257);
301    }
302
303    public void testNoop() throws Exception {
304        ftpClientConnect();
305
306        // NOOP
307        boolean success = ftpClient.sendNoOp();
308        assertTrue("Unable to NOOP", success);
309        verifyReplyCode("NOOP", 200);
310    }
311
312    public void testRest() throws Exception {
313        ftpClientConnect();
314
315        // REST
316        int replyCode = ftpClient.rest("marker");
317        assertEquals("Unable to REST", 350, replyCode);
318    }
319
320    public void testRmd() throws Exception {
321        ftpClientConnect();
322
323        // RMD
324        boolean success = ftpClient.removeDirectory("dir1/dir2");
325        assertTrue("Unable to RMD", success);
326        verifyReplyCode("removeDirectory", 250);
327    }
328
329    public void testRename() throws Exception {
330        ftpClientConnect();
331
332        // Rename (RNFR, RNTO)
333        boolean success = ftpClient.rename(FILENAME, "new_" + FILENAME);
334        assertTrue("Unable to RENAME", success);
335        verifyReplyCode("rename", 250);
336    }
337
338    public void testAllo() throws Exception {
339        ftpClientConnect();
340
341        // ALLO
342        assertTrue("ALLO", ftpClient.allocate(1024));
343        assertTrue("ALLO with recordSize", ftpClient.allocate(1024, 64));
344    }
345
346    /**
347     * Test GET and PUT of ASCII files
348     */
349    public void testTransferAsciiFile() throws Exception {
350        retrCommandHandler.setFileContents(ASCII_CONTENTS);
351
352        ftpClientConnect();
353
354        // Get File
355        LOG.info("Get File for remotePath [" + FILENAME + "]");
356        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
357        assertTrue(ftpClient.retrieveFile(FILENAME, outputStream));
358        LOG.info("File contents=[" + outputStream.toString());
359        assertEquals("File contents", ASCII_CONTENTS, outputStream.toString());
360
361        // Put File
362        LOG.info("Put File for local path [" + FILENAME + "]");
363        ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes());
364        assertTrue(ftpClient.storeFile(FILENAME, inputStream));
365        InvocationRecord invocationRecord = storCommandHandler.getInvocation(0);
366        byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY);
367        LOG.info("File contents=[" + contents + "]");
368        assertEquals("File contents", ASCII_CONTENTS.getBytes(), contents);
369    }
370
371    /**
372     * Test GET and PUT of binary files
373     */
374    public void testTransferBinaryFiles() throws Exception {
375        retrCommandHandler.setFileContents(BINARY_CONTENTS);
376
377        ftpClientConnect();
378        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
379
380        // Get File
381        LOG.info("Get File for remotePath [" + FILENAME + "]");
382        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
383        assertTrue("GET", ftpClient.retrieveFile(FILENAME, outputStream));
384        LOG.info("GET File length=" + outputStream.size());
385        assertEquals("File contents", BINARY_CONTENTS, outputStream.toByteArray());
386
387        // Put File
388        LOG.info("Put File for local path [" + FILENAME + "]");
389        ByteArrayInputStream inputStream = new ByteArrayInputStream(BINARY_CONTENTS);
390        assertTrue("PUT", ftpClient.storeFile(FILENAME, inputStream));
391        InvocationRecord invocationRecord = storCommandHandler.getInvocation(0);
392        byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY);
393        LOG.info("PUT File length=" + contents.length);
394        assertEquals("File contents", BINARY_CONTENTS, contents);
395    }
396
397    public void testStou() throws Exception {
398        StouCommandHandler stouCommandHandler = (StouCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STOU);
399        stouCommandHandler.setFilename(FILENAME);
400
401        ftpClientConnect();
402
403        // Stor a File (STOU)
404        ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes());
405        assertTrue(ftpClient.storeUniqueFile(FILENAME, inputStream));
406        InvocationRecord invocationRecord = stouCommandHandler.getInvocation(0);
407        byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY);
408        LOG.info("File contents=[" + contents + "]");
409        assertEquals("File contents", ASCII_CONTENTS.getBytes(), contents);
410    }
411
412    public void testAppe() throws Exception {
413        AppeCommandHandler appeCommandHandler = (AppeCommandHandler) stubFtpServer.getCommandHandler(CommandNames.APPE);
414
415        ftpClientConnect();
416
417        // Append a File (APPE)
418        ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes());
419        assertTrue(ftpClient.appendFile(FILENAME, inputStream));
420        InvocationRecord invocationRecord = appeCommandHandler.getInvocation(0);
421        byte[] contents = (byte[]) invocationRecord.getObject(AppeCommandHandler.FILE_CONTENTS_KEY);
422        LOG.info("File contents=[" + contents + "]");
423        assertEquals("File contents", ASCII_CONTENTS.getBytes(), contents);
424    }
425
426    public void testAbor() throws Exception {
427        ftpClientConnect();
428
429        // ABOR
430        assertTrue("ABOR", ftpClient.abort());
431    }
432
433    public void testPasv() throws Exception {
434        ftpClientConnect();
435
436        // PASV
437        ftpClient.enterLocalPassiveMode();
438        // no reply code; the PASV command is sent only when the data connection is opened
439    }
440
441    public void testMode() throws Exception {
442        ftpClientConnect();
443
444        // MODE
445        boolean success = ftpClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);
446        assertTrue("Unable to MODE", success);
447        verifyReplyCode("setFileTransferMode", 200);
448    }
449
450    public void testStru() throws Exception {
451        ftpClientConnect();
452
453        // STRU
454        boolean success = ftpClient.setFileStructure(FTP.FILE_STRUCTURE);
455        assertTrue("Unable to STRU", success);
456        verifyReplyCode("setFileStructure", 200);
457    }
458
459    public void testSimpleCompositeCommandHandler() throws Exception {
460        // Replace CWD CommandHandler with a SimpleCompositeCommandHandler
461        CommandHandler commandHandler1 = new StaticReplyCommandHandler(500);
462        CommandHandler commandHandler2 = new CwdCommandHandler();
463        SimpleCompositeCommandHandler simpleCompositeCommandHandler = new SimpleCompositeCommandHandler();
464        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);
465        simpleCompositeCommandHandler.addCommandHandler(commandHandler2);
466        stubFtpServer.setCommandHandler("CWD", simpleCompositeCommandHandler);
467
468        // Connect
469        ftpClientConnect();
470
471        // CWD
472        assertFalse("first", ftpClient.changeWorkingDirectory("dir1/dir2"));
473        assertTrue("first", ftpClient.changeWorkingDirectory("dir1/dir2"));
474    }
475
476    public void testSite() throws Exception {
477        ftpClientConnect();
478
479        // SITE
480        int replyCode = ftpClient.site("parameters,1,2,3");
481        assertEquals("SITE", 200, replyCode);
482    }
483
484    public void testSmnt() throws Exception {
485        ftpClientConnect();
486
487        // SMNT
488        assertTrue("SMNT", ftpClient.structureMount("dir1/dir2"));
489        verifyReplyCode("structureMount", 250);
490    }
491
492    public void testRein() throws Exception {
493        ftpClientConnect();
494
495        // REIN
496        assertEquals("REIN", 220, ftpClient.rein());
497    }
498
499    /**
500     * Test that command names in lowercase or mixed upper/lower case are accepted
501     */
502    public void testCommandNamesInLowerOrMixedCase() throws Exception {
503        ftpClientConnect();
504
505        assertEquals("rein", 220, ftpClient.sendCommand("rein"));
506        assertEquals("rEIn", 220, ftpClient.sendCommand("rEIn"));
507        assertEquals("reiN", 220, ftpClient.sendCommand("reiN"));
508        assertEquals("Rein", 220, ftpClient.sendCommand("Rein"));
509    }
510
511    public void testUnrecognizedCommand() throws Exception {
512        ftpClientConnect();
513
514        assertEquals("Unrecognized:XXXX", 502, ftpClient.sendCommand("XXXX"));
515    }
516
517    // -------------------------------------------------------------------------
518    // Test setup and tear-down
519    // -------------------------------------------------------------------------
520
521    /**
522     * Perform initialization before each test
523     *
524     * @see org.mockftpserver.test.AbstractTestCase#setUp()
525     */
526    protected void setUp() throws Exception {
527        super.setUp();
528
529        for (int i = 0; i < BINARY_CONTENTS.length; i++) {
530            BINARY_CONTENTS[i] = (byte) i;
531        }
532
533        stubFtpServer = new StubFtpServer();
534        stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort());
535        stubFtpServer.start();
536        ftpClient = new FTPClient();
537        retrCommandHandler = (RetrCommandHandler) stubFtpServer.getCommandHandler(CommandNames.RETR);
538        storCommandHandler = (StorCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STOR);
539    }
540
541    /**
542     * Perform cleanup after each test
543     *
544     * @see org.mockftpserver.test.AbstractTestCase#tearDown()
545     */
546    protected void tearDown() throws Exception {
547        super.tearDown();
548        stubFtpServer.stop();
549    }
550
551    // -------------------------------------------------------------------------
552    // Internal Helper Methods
553    // -------------------------------------------------------------------------
554
555    /**
556     * Connect to the server from the FTPClient
557     */
558    private void ftpClientConnect() throws IOException {
559        ftpClient.connect(SERVER, PortTestUtil.getFtpServerControlPort());
560    }
561
562    /**
563     * Assert that the FtpClient reply code is equal to the expected value
564     *
565     * @param operation         - the description of the operation performed; used in the error message
566     * @param expectedReplyCode - the expected FtpClient reply code
567     */
568    private void verifyReplyCode(String operation, int expectedReplyCode) {
569        int replyCode = ftpClient.getReplyCode();
570        LOG.info("Reply: operation=\"" + operation + "\" replyCode=" + replyCode);
571        assertEquals("Unexpected replyCode for " + operation, expectedReplyCode, replyCode);
572    }
573
574    /**
575     * Verify that the FTPFile has the specified properties
576     *
577     * @param ftpFile - the FTPFile to verify
578     * @param type    - the expected file type
579     * @param name    - the expected file name
580     * @param size    - the expected file size (will be zero for a directory)
581     */
582    private void verifyFTPFile(FTPFile ftpFile, int type, String name, long size) {
583        LOG.info(ftpFile);
584        assertEquals("type: " + ftpFile, type, ftpFile.getType());
585        assertEquals("name: " + ftpFile, name, ftpFile.getName());
586        assertEquals("size: " + ftpFile, size, ftpFile.getSize());
587    }
588
589}
590