Compare commits

...

32 Commits

Author SHA1 Message Date
Tyler Stewart
cf3d543f1a fix(commands): correctly clone command for log 2017-05-05 18:04:54 -06:00
Tyler Stewart
69bec2b01c fix(fs): normalize fs paths
- Attempting to fix compatability on windows
2017-05-04 17:43:46 -06:00
Tyler Stewart
2eac41d127 test: update test setup
master
2017-05-04 17:43:41 -06:00
Tyler Stewart
eb32f93fc6 feat: close server on SIGINT (ctrl+c) 2017-05-04 17:42:47 -06:00
Tyler Stewart
095423606e fix(find-port): stop check at 65535 2017-05-04 17:42:10 -06:00
Tyler Stewart
61cf1bda39 feat(connection): add helper get for socket remote address 2017-05-04 17:40:37 -06:00
Tyler Stewart
75f847ed5d feat(commands): obfuscate password from logs 2017-05-04 17:40:01 -06:00
Tyler Stewart
ad4b32fc13 chore: add .env to github for tests 2017-05-04 17:39:15 -06:00
Tyler Stewart
be3c57bed0 Merge pull request #10 from stewarttylerr/sandbotorg-master
Sandbotorg master
2017-04-27 13:27:46 -06:00
Tyler Stewart
dc7dd1075c test(passive): merge master 2017-04-27 13:22:57 -06:00
salper
543e6cc1cc chore(readme): fix grammar 2017-04-27 13:22:39 -06:00
salper
5c1f8f7a65 fix: plug QUIT command
master
2017-04-27 13:19:29 -06:00
Tyler Stewart
557995a1a9 test(travis): add env variables 2017-04-27 13:17:01 -06:00
Tyler Stewart
45eca5afe0 test(passive): fix test 2017-04-27 12:49:55 -06:00
Tyler Stewart
695e594d97 Merge pull request #6 from stewarttylerr/migate-to-moment
feat: migrate to moment from date-fns, fix ls format
2017-03-31 17:14:44 -06:00
Tyler Stewart
97b55fc92c feat: migrate to moment from date-fns, fix ls format
Date-fns is great, but too early for use
2017-03-31 17:12:57 -06:00
Tyler Stewart
577066850b fix: improve getting current directory 2017-03-30 12:26:04 -06:00
Tyler Stewart
0ec989cf1e docs: update login event for new root option 2017-03-29 10:20:22 -06:00
Tyler Stewart
568833e216 Merge pull request #4 from stewarttylerr/set-fs-root
feat(fs): allow default file system root to be set
2017-03-29 10:15:33 -06:00
Tyler Stewart
6b0c06e588 fix: update tests 2017-03-29 10:13:32 -06:00
Tyler Stewart
acd485a571 test: more tests
set-fs-root
2017-03-28 14:27:40 -06:00
Tyler Stewart
2b2ca45673 test: update and add tests 2017-03-27 17:57:03 -06:00
Tyler Stewart
a62b6f9559 fix: resolve disconnectClient promise, linting 2017-03-27 17:51:10 -06:00
Tyler Stewart
84d54cbc2b fix(TYPE): correctly set encoding 2017-03-27 17:51:10 -06:00
Tyler Stewart
ef6134d91b fix: wrap fs calls with when, linting 2017-03-27 17:51:10 -06:00
Tyler Stewart
043d9369cc chore(readme): minor text fixes 2017-03-27 17:51:10 -06:00
Tyler Stewart
6b81748fd7 chore: minor text fixes 2017-03-27 17:51:10 -06:00
Tyler Stewart
0f4f5cdbd7 chore(readme): update api explainations
master
2017-03-27 17:51:10 -06:00
Tyler Stewart
0293752635 chore(readme): minor text fixes 2017-03-27 13:57:54 -06:00
Tyler Stewart
aa278105f9 chore: minor text fixes 2017-03-27 13:56:51 -06:00
Tyler Stewart
bbe0bf2942 chore(readme): update api explainations
master
2017-03-16 14:09:31 -06:00
Tyler Stewart
846df72e24 feat(fs): allow default file system root to be set
Enables users to only access portions of the file system (eg /home/user)
2017-03-13 13:29:15 -06:00
83 changed files with 1507 additions and 415 deletions

3
.env Executable file
View File

@@ -0,0 +1,3 @@
FTP_URL=ftp://127.0.0.1:8880
PASV_RANGE=8881
LOG_LEVEL=fatal

1
.gitignore vendored
View File

@@ -2,5 +2,4 @@ node_modules/
dist/
reports/
.env
npm-debug.log

View File

@@ -2,6 +2,10 @@ language: node_js
node_js:
- "6"
env:
FTP_URL: ftp://127.0.0.1:8880
PASV_RANGE: 8881
install: npm install
script:

172
README.md
View File

@@ -1,35 +1,39 @@
# ftp-srv [![npm version](https://badge.fury.io/js/ftp-srv.svg)](https://badge.fury.io/js/ftp-srv) [![Build Status](https://travis-ci.org/stewarttylerr/ftp-srv.svg?branch=master)](https://travis-ci.org/stewarttylerr/ftp-srv) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
<!--[RM_DESCRIPTION]-->
> Modern, extensible FTP Server
<!--[]-->
- [Overview](#overview)
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [Events](#events)
- [File System](#file-system)
- [Contributing](#contributing)
- [License](#license)
## Overview
> `ftp-srv` is designed to be easy, exensible, and modern.
> Configuration is very minimal for a basic FTP server,
but can easily grow to fit a larger scale project.
## Features
- Supports passive and active connections
- Extensible [file system](#file-system)
- Allows extensible [file systems](#file-system) on a per connection basis
## Install
`npm install ftp-srv --save`
`yarn add ftp-srv`
`npm install ftp-srv --save`
## Usage
- [Options](#options)
- [Events](#events)
- [File System](#file-system)
```js
// Quick start
const FtpSvr = require('ftp-srv');
const ftpServer = new FtpSvr([url], { [options] ... });
const ftpServer = new FtpSvr(url, [{ options ... }]);
ftpServer.on('...', (data, resolve, reject) => { ... })
@@ -37,112 +41,130 @@ ftpServer.listen()
.then(() => { ... });
```
### Options
__url__ : `ftp://127.0.0.1:21`
> Host and port to listen on and make passive connections to.
Set the hostname to "0.0.0.0" to fetch the external IP automatically: `ftp://0.0.0.0:21`
## API
__pasv_range__ : `22`
> Minimum port or range to use for passive connections.
Provide either a starting integer (`1000`) or a range (`1000-2000`).
#### new FtpSrv(url, [options])
__anonymous__ : `false`
> Set whether a valid username or password combination is required.
If true, will not require the `PASS` command to be sent for login.
- __url__ :: `ftp://127.0.0.1:21`
- A full href url, indicating the protocol, and external IP with port to listen for connections.
- Supported protocols:
- `ftp`
- To accept external connections, the hostname must be the box's external IP address. This can be fetched automatically by setting the hostname to `0.0.0.0`.
- __options__ :: `{}`
- __pasv_range__ :: `22`
- Starting port or min - max range to accept passive connections
- Ports will be queried for an unused port in the range to use for the connection.
- If none are found, the connection cannot be established
- If an integer is supplied: will indicate the minimum allowable port
- If a range is defined (`3000-3100`): only ports within that range will be used
- __anonymous__ :: `false`
- If true, will authenticate connections after passing the `USER` command. Passwords will not be required.
- __blacklist__ :: `[]`
- Array of commands to be blacklisted globally
- `['RMD', 'RNFR', 'RNTO']`
- A connection sending one of these commands will be replied with code `502`
- __whitelist__ :: `[]`
- If set, only commands within this array are allowed
- A connection sending any other command will be replied to with code `502`
- __file_format__ :: `ls`
- Set the format to use for file stat queries, such as `LIST`
- Possible values include:
- `ls` : [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
- `ep` : [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
- Function : pass in a function as the parameter to use your own
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
- __log__ :: `bunyan.createLogger()`
- A [bunyan logger](https://github.com/trentm/node-bunyan) instance
- By default, one is created, but a custom instance can be passed in as well
__blacklist__ : `[]`
> Commands listed will not be allowed.
`['RMD', 'RNFR', 'RNTO']`
## Events
__whitelist__ : `[]`
> If set, no other commands are allowed except for those explicitly listed.
`['USER', 'PASS', 'PWD']`
#### "login" ({connection, username, password}, resolve, reject)
> Occurs after `PASS` command is set, or after `USER` if `anonymous` is `true`
__file_format__ : `ls`
> Format to use for [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) responses (such as with the `LIST` command).
Possible values:
`ls` : [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
`ep` : [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
`function` : pass in your own format function, returning a string:
`function (fileStats) { ... }`
- __connection__
- Instance of the FTP client
- __username__
- Username provided in the `USER` command
- __password__
- Password provided in the `PASS` command
- Only provided if `anonymous` is set to `false`
- __resolve ({fs, root, cwd, blacklist, whitelist})__
- __fs__ _[optional]_
- Optional file system class for connection to use
- See [File System](#file-system) for implementation details
- __root__ _[optional]_
- If `fs` not provided, will set the root directory for the connection
- The user cannot traverse lower than this directory
- __cwd__ _[optional]_
- If `fs` not provided, will set the starting directory for the connection
- __blacklist__ _[optional]_
- Commands that are forbidden for this connection only
- __whitelist__ _[optional]_
- If set, this connection will only be able to use the provided commands
- __reject (error)__
- __error__
- Error object
__log__ : `bunyan`
> A [bunyan logger](https://github.com/trentm/node-bunyan) instance.
### Events
All events emit the same structure: `({data object}, resolve, reject)`
__login__ : `{connection, username, password}`
> Occurs after `PASV` (or `USER` if `options.anonymous`)
```
resolve({
fs, // [optional] custom file system class
cwd, // [optional] initial working directory (if not using custom file system),
blacklist, // [optional] commands to forbid for this connection only
whitelist // [optional] if set, only these commands are allowed for this connection only
})
```
### File System
The file system can be overridden to use your own custom class. This an allow for interacting with files without actually writing them.
*Anytime a [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object is used, it must have added `name` property with the file's name.*
## File System
> The default file system can be overriden to use your own implementation. This can allow for virtual file systems and more.
> Each connection can be given it's own file system depending on the user.
#### Functions
`currentDirectory()`
> Returns a string of the current working directory
Returns a string of the current working directory
> Used in: `PWD`
`get(fileName)`
> Returns a file stat object of file or directory
`get(fileName)`
Returns a file stat object of file or directory
> Used in: `STAT`, `SIZE`, `RNFR`, `MDTM`
`list(path)`
> Returns array of file and directory stat objects
`list(path)`
Returns array of file and directory stat objects
> Used in `LIST`, `STAT`
`chdir(path)`
> Returns new directory relative to cwd
`chdir(path)`
Returns new directory relative to cwd
> Used in `CWD`, `CDUP`
`mkdir(path)`
> Return a path to a newly created directory
`mkdir(path)`
Returns a path to a newly created directory
> Used in `MKD`
`write(fileName, options)`
> Returns a writable stream
`write(fileName, options)`
Returns a writable stream
Options:
`append` if true, append to existing file
> Used in `STOR`, `APPE`
`read(fileName)`
> Returns a readable stream
`read(fileName)`
Returns a readable stream
> Used in `RETR`
`delete(path)`
> Delete a file or directory
`delete(path)`
Delete a file or directory
> Used in `DELE`
`rename(from, to)`
> Rename a file or directory
`rename(from, to)`
Rename a file or directory
> Used in `RNFR`, `RNTO`
`chmod(path)`
> Modify a file or directory's permissions
`chmod(path)`
Modify a file or directory's permissions
> Used in `SITE CHMOD`
`getUniqueName()`
> Return a unique file name to write to
`getUniqueName()`
Returns a unique file name to write to
> Used in `STOU`

View File

@@ -46,10 +46,10 @@
}
},
"dependencies": {
"bunyan": "^1.8.5",
"date-fns": "^1.28.0",
"bunyan": "^1.8.9",
"lodash": "^4.17.4",
"minimist-string": "^1.0.2",
"moment": "^2.18.1",
"uuid": "^3.0.1",
"when": "^3.7.8"
},
@@ -71,8 +71,7 @@
"npm-run-all": "4.0.1",
"rimraf": "2.5.4",
"semantic-release": "^6.3.2",
"sinon": "^1.17.7",
"sinon-as-promised": "^4.0.2"
"sinon": "^2.1.0"
},
"engines": {
"node": ">=6.x",

View File

@@ -12,8 +12,13 @@ class FtpCommands {
}
handle(command) {
const log = this.connection.log.child({command});
log.trace('Handle command');
// Obfuscate password from logs
const logCommand = _.cloneDeep(command);
command.directive = _.upperCase(command._[0]);
if (command.directive === 'PASS') logCommand._[1] = '********';
const log = this.connection.log.child({directive: command.directive});
log.trace({command: logCommand}, 'Handle command');
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(402, 'Command not allowed');
@@ -40,7 +45,7 @@ class FtpCommands {
const handler = commandRegister.handler.bind(this.connection);
return when.try(handler, { log, command, previous_command: this.previousCommand })
.finally(() => {
this.previousCommand = _.clone(command);
this.previousCommand = _.cloneDeep(command);
});
}
}

View File

@@ -7,4 +7,4 @@ module.exports = {
},
syntax: '{{cmd}} [path]',
description: 'Append to a file'
}
};

View File

@@ -16,7 +16,7 @@ module.exports = {
flags: {
no_auth: true
}
}
};
function handleTLS() {
return this.reply(504);

View File

@@ -2,10 +2,10 @@ const cwd = require('./cwd').handler;
module.exports = {
directive: ['CDUP', 'XCUP'],
handler: function(args) {
handler: function (args) {
args.command._ = [args.command._[0], '..'];
return cwd.call(this, args);
},
syntax: '{{cmd}}',
description: 'Change to Parent Directory'
}
};

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
return when(this.fs.chdir(command._[1]))
return when.try(this.fs.chdir.bind(this.fs), command._[1])
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path);
@@ -19,4 +19,4 @@ module.exports = {
},
syntax: '{{cmd}}[path]',
description: 'Change working directory'
}
};

View File

@@ -6,7 +6,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
return when(this.fs.delete(command._[1]))
return when.try(this.fs.delete.bind(this.fs), command._[1])
.then(() => {
return this.reply(250);
})
@@ -17,4 +17,4 @@ module.exports = {
},
syntax: '{{cmd}} [path]',
description: 'Delete file'
}
};

View File

@@ -18,4 +18,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -21,4 +21,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -6,7 +6,7 @@ const getFileStat = require('../../helpers/file-stat');
// http://cr.yp.to/ftp/list/eplf.html
module.exports = {
directive: 'LIST',
handler: function ({log, command, previous_command} = {}) {
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
@@ -19,9 +19,9 @@ module.exports = {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.list(directory)))
.then(() => when.try(this.fs.list.bind(this.fs), directory))
.then(files => {
const getFileMessage = (file) => {
const getFileMessage = file => {
if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
@@ -33,7 +33,7 @@ module.exports = {
message,
socket: dataSocket
};
})
});
return this.reply(150)
.then(() => {
if (fileList.length) return this.reply({}, ...fileList);
@@ -57,4 +57,4 @@ module.exports = {
},
syntax: '{{cmd}} [path(optional)]',
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
}
};

View File

@@ -1,5 +1,5 @@
const when = require('when');
const format = require('date-fns/format');
const moment = require('moment');
module.exports = {
directive: 'MDTM',
@@ -7,10 +7,10 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(command._[1]))
return when.try(this.fs.get.bind(this.fs), command._[1])
.then(fileStat => {
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime)
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime);
})
.catch(err => {
log.error(err);
@@ -22,4 +22,4 @@ module.exports = {
flags: {
feat: 'MDTM'
}
}
};

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return when(this.fs.mkdir(command._[1]))
return when.try(this.fs.mkdir.bind(this.fs), command._[1])
.then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);
@@ -19,4 +19,4 @@ module.exports = {
},
syntax: '{{cmd}}[path]',
description: 'Make directory'
}
};

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
obsolete: true
}
}
};

View File

@@ -7,4 +7,4 @@ module.exports = {
},
syntax: '{{cmd}} [path(optional)]',
description: 'Returns a list of file names in a specified directory'
}
};

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -5,4 +5,4 @@ module.exports = {
},
syntax: '{{cmd}}',
description: 'Select options for a feature'
}
};

View File

@@ -24,4 +24,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -2,7 +2,7 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'PASV',
handler: function ({command} = {}) {
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
@@ -17,4 +17,4 @@ module.exports = {
},
syntax: '{{cmd}}',
description: 'Initiate passive mode'
}
};

View File

@@ -1,10 +1,11 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = command._[1].split(',');
const rawConnection = _.get(command, '_[1]', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.');
@@ -12,10 +13,8 @@ module.exports = {
const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port)
.then(socket => {
return this.reply(200);
})
.then(() => this.reply(200));
},
syntax: '{{cmd}} [x,x,x,x,y,y]',
syntax: '{{cmd}} x,x,x,x,y,y',
description: 'Specifies an address and port to which the server should connect'
}
};

View File

@@ -3,11 +3,11 @@ const escapePath = require('../../helpers/escape-path');
module.exports = {
directive: ['PWD', 'XPWD'],
handler: function ({log, command} = {}) {
handler: function ({log} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
return when(this.fs.currentDirectory())
return when.try(this.fs.currentDirectory.bind(this.fs))
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(257, path);
@@ -15,8 +15,8 @@ module.exports = {
.catch(err => {
log.error(err);
return this.reply(550, err.message);
})
});
},
syntax: '{{cmd}}',
description: 'Print current working directory'
}
};

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -12,7 +12,7 @@ module.exports = {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.read(command._[1])))
.then(() => when.try(this.fs.read.bind(this.fs), command._[1]))
.then(stream => {
return when.promise((resolve, reject) => {
dataSocket.on('error', err => stream.emit('error', err));
@@ -34,8 +34,8 @@ module.exports = {
.finally(() => {
this.connector.end();
this.commandSocket.resume();
})
});
},
syntax: '{{cmd}} [path]',
description: 'Retrieve a copy of the file'
}
};

View File

@@ -7,4 +7,4 @@ module.exports = {
},
syntax: '{{cmd}} [path]',
description: 'Remove a directory'
}
};

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
const fileName = command._[1];
return when(this.fs.get(fileName))
return when.try(this.fs.get.bind(this.fs), fileName)
.then(() => {
this.renameFrom = fileName;
return this.reply(350);

View File

@@ -11,7 +11,7 @@ module.exports = {
const from = this.renameFrom;
const to = command._[1];
return when(this.fs.rename(from, to))
return when.try(this.fs.rename.bind(this.fs), from, to)
.then(() => {
return this.reply(250);
})
@@ -21,8 +21,8 @@ module.exports = {
})
.finally(() => {
delete this.renameFrom;
})
});
},
syntax: '{{cmd}} [name]',
description: 'Rename to'
}
};

View File

@@ -1,14 +1,16 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
const [, mode, fileName] = command._;
return this.fs.chmod(fileName, parseInt(mode, 8))
return when.try(this.fs.chmod.bind(this.fs), fileName, parseInt(mode, 8))
.then(() => {
return this.reply(200);
})
.catch(err => {
log.error(err);
return this.reply(500);
})
});
};

View File

@@ -14,10 +14,10 @@ module.exports = {
const subCommand = {
_: [subverb, ...subparameters],
directive: subverb
}
};
const handler = registry[subverb].handler.bind(this);
return when.try(handler, { log: subLog, command: subCommand });
},
syntax: '{{cmd}} [subVerb] [subParams]',
description: 'Sends site specific commands to remote server'
}
};

View File

@@ -6,7 +6,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(command._[1]))
return when.try(this.fs.get.bind(this.fs), command._[1])
.then(fileStat => {
return this.reply(213, {message: fileStat.size});
})
@@ -20,4 +20,4 @@ module.exports = {
flags: {
feat: 'SIZE'
}
}
};

View File

@@ -6,15 +6,17 @@ module.exports = {
directive: 'STAT',
handler: function (args = {}) {
const {log, command} = args;
const path = command._[1];
const path = _.get(command, '_[1]');
if (path) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(path))
return when.try(this.fs.get.bind(this.fs), path)
.then(stat => {
if (stat.isDirectory()) {
return when(this.fs.list(path))
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.list.bind(this.fs), path)
.then(files => {
const fileList = files.map(file => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
@@ -22,21 +24,21 @@ module.exports = {
raw: true,
message
};
})
});
return this.reply(213, 'Status begin', ...fileList, 'Status end');
})
});
} else {
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')))
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')));
}
})
.catch(err => {
log.error(err);
return this.reply(450);
})
});
} else {
return this.reply(211, 'Status OK');
}
},
syntax: '{{cmd}} [path(optional)]',
description: 'Returns the current status'
}
};

View File

@@ -15,7 +15,7 @@ module.exports = {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.write(fileName, {append})))
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append}))
.then(stream => {
return when.promise((resolve, reject) => {
stream.on('error', err => dataSocket.emit('error', err));
@@ -41,4 +41,4 @@ module.exports = {
},
syntax: '{{cmd}} [path]',
description: 'Store data as a file at the server site'
}
};

View File

@@ -1,3 +1,5 @@
const when = require('when');
const stor = require('./stor').handler;
module.exports = {
@@ -7,9 +9,11 @@ module.exports = {
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
const fileName = args.command._[1];
return this.fs.get(fileName)
.catch(() => fileName) // does not exist, name is unique
.then(() => this.fs.getUniqueName()) // exists, must create new unique name
return when.try(() => {
return when.try(this.fs.get.bind(this.fs), fileName)
.then(() => when.try(this.fs.getUniqueName.bind(this.fs)))
.catch(() => when.resolve(fileName));
})
.then(name => {
args.command._[1] = name;
return stor.call(this, args);

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
obsolete: true
}
}
};

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -1,20 +1,20 @@
const _ = require('lodash');
const ENCODING_TYPES = {
A: 'utf-8',
I: 'binary',
L: 'binary'
};
module.exports = {
directive: 'TYPE',
handler: function ({command} = {}) {
const encoding = _.upperCase(command._[1]);
switch (encoding) {
case 'A':
this.encoding = 'utf-8';
case 'I':
case 'L':
this.encoding = 'binary';
return this.reply(200);
default:
return this.reply(501);
}
if (!ENCODING_TYPES.hasOwnProperty(encoding)) return this.reply(501);
this.encoding = ENCODING_TYPES[encoding];
return this.reply(200);
},
syntax: '{{cmd}} [mode]',
description: 'Set the transfer mode, binary (I) or utf-8 (A)'
}
};

View File

@@ -2,7 +2,10 @@ module.exports = {
directive: 'USER',
handler: function ({log, command} = {}) {
if (this.username) return this.reply(530, 'Username already set');
this.username = command._[1];
if (!this.username) return this.reply(501, 'Must send username requirement');
if (this.server.options.anonymous === true) {
return this.login(this.username, '@anonymous')
.then(() => {
@@ -20,4 +23,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

@@ -1,3 +1,4 @@
/* eslint no-return-assign: 0 */
const commands = [
require('./registration/abor'),
require('./registration/allo'),
@@ -19,6 +20,7 @@ const commands = [
require('./registration/pasv'),
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),

View File

@@ -3,7 +3,6 @@ const uuid = require('uuid');
const when = require('when');
const sequence = require('when/sequence');
const parseCommandString = require('minimist-string');
const net = require('net');
const BaseConnector = require('./connector/base');
const FileSystem = require('./fs');
@@ -16,7 +15,7 @@ class FtpConnection {
this.server = server;
this.commandSocket = options.socket;
this.id = uuid.v4();
this.log = options.log.child({id: this.id});
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.encoding = 'utf-8';
@@ -27,9 +26,8 @@ class FtpConnection {
});
this.commandSocket.on('data', data => {
const messages = _.compact(data.toString('utf-8').split('\r\n'));
const handleMessage = (message) => {
const handleMessage = message => {
const command = parseCommandString(message);
command.directive = _.upperCase(command._[0]);
return this.commands.handle(command);
};
@@ -42,13 +40,19 @@ class FtpConnection {
});
}
get ip() {
try {
return this.commandSocket.remoteAddress;
} catch (ex) {
return null;
}
}
close(code = 421, message = 'Closing connection') {
return when(() => {
if (code) return this.reply(code, message);
})
.then(() => {
if (this.commandSocket) this.commandSocket.end();
});
return when
.resolve(code)
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {
@@ -60,11 +64,11 @@ class FtpConnection {
return this.server.emit('login', {connection: this, username, password});
}
})
.then(({fs, cwd, blacklist = [], whitelist = []} = {}) => {
.then(({root = '/', cwd = '/', fs, blacklist = [], whitelist = []} = {}) => {
this.authenticated = true;
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
this.fs = fs || new FileSystem(this, {cwd});
this.fs = fs || new FileSystem(this, {root, cwd});
});
}
@@ -73,7 +77,7 @@ class FtpConnection {
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters];
if (!letters.length) letters = [{}];
return when.map(letters, (promise, index) => {
return when.map(letters, promise => {
return when(promise)
.then(letter => {
if (!letter) letter = {};
@@ -86,16 +90,16 @@ class FtpConnection {
.then(message => {
letter.message = message;
return letter;
})
});
});
});
}
};
const processLetter = (letter, index) => {
return when.promise((resolve, reject) => {
const seperator = !options.hasOwnProperty('eol') ?
(letters.length - 1 === index ? ' ' : '-') :
(options.eol ? ' ' : '-');
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
if (letter.socket && letter.socket.writable) {
@@ -109,10 +113,10 @@ class FtpConnection {
});
} else reject(new errors.SocketError('Socket not writable'));
});
}
};
return satisfyParameters()
.then(letters => sequence(letters.map((letter, index) => processLetter.bind(this, letter, index))))
.then(satisfiedLetters => sequence(satisfiedLetters.map((letter, index) => processLetter.bind(this, letter, index))))
.catch(err => {
this.log.error(err);
});

View File

@@ -1,4 +1,4 @@
const net = require('net');
const {Socket} = require('net');
const when = require('when');
const Connector = require('./base');
@@ -20,11 +20,11 @@ class Active extends Connector {
setupConnection(host, port) {
const closeExistingServer = () => this.dataSocket ?
when(this.dataSocket.destroy()) :
when.resolve()
when.resolve();
return closeExistingServer()
.then(() => {
this.dataSocket = new net.Socket();
this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.encoding);
this.dataSocket.connect({ host, port }, () => {
this.dataSocket.pause();

View File

@@ -25,7 +25,7 @@ class Passive extends Connector {
setupServer() {
const closeExistingServer = () => this.dataServer ?
when.promise(resolve => this.dataServer.close(() => resolve())) :
when.resolve()
when.resolve();
return closeExistingServer()
.then(() => this.getPort())
@@ -78,7 +78,7 @@ class Passive extends Connector {
[this.server.options.pasv_range];
return findPort(min, max);
} else return undefined;
};
}
}
module.exports = Passive;

View File

@@ -9,10 +9,28 @@ const errors = require('./errors');
class FileSystem {
constructor(connection, {
root = '/',
cwd = '/'
} = {}) {
this.connection = connection;
this.cwd = cwd;
this.cwd = this._normalize(cwd);
this.root = this._normalize(root);
}
_normalize(path) {
return nodePath.normalize(path
.replace(/\\/g, '\/') // replaces \ with /
.replace(/\\\\/g, '\/') // replaces \\ with /
.replace(/\/\//g, '\/') // replaces // with /
);
}
_resolvePath(path) {
const pathParts = {
root: this.root,
base: nodePath.join(this.cwd, this._normalize(path))
};
return nodePath.format(pathParts);
}
currentDirectory() {
@@ -20,13 +38,13 @@ class FileSystem {
}
get(fileName) {
const path = nodePath.resolve(this.cwd, fileName);
const path = this._resolvePath(fileName);
return fs.stat(path)
.then(stat => _.set(stat, 'name', fileName));
}
list(path = '.') {
path = nodePath.resolve(this.cwd, path);
path = this._resolvePath(path);
return fs.readdir(path)
.then(fileNames => {
return when.map(fileNames, fileName => {
@@ -43,26 +61,26 @@ class FileSystem {
}
chdir(path = '.') {
path = nodePath.resolve(this.cwd, path);
path = this._resolvePath(path);
return fs.stat(path)
.tap(stat => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = path;
return this.cwd;
this.cwd = path.substring(this.root.length) || '/';
return this.currentDirectory();
});
}
write(fileName, {append = false} = {}) {
const path = nodePath.resolve(this.cwd, fileName);
const path = this._resolvePath(fileName);
const stream = syncFs.createWriteStream(path, {flags: !append ? 'w+' : 'a+'});
stream.on('error', () => fs.unlink(path));
return stream;
}
read(fileName) {
const path = nodePath.resolve(this.cwd, fileName);
const path = this._resolvePath(fileName);
return fs.stat(path)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
@@ -74,28 +92,28 @@ class FileSystem {
}
delete(path) {
path = nodePath.resolve(this.cwd, path);
path = this._resolvePath(path);
return fs.stat(path)
.then(stat => {
if (stat.isDirectory()) return fs.rmdir(path);
else return fs.unlink(path);
})
});
}
mkdir(path) {
path = nodePath.resolve(this.cwd, path);
path = this._resolvePath(path);
return fs.mkdir(path)
.then(() => path);
}
rename(from, to) {
const fromPath = nodePath.resolve(this.cwd, from);
const toPath = nodePath.resolve(this.cwd, to);
const fromPath = this._resolvePath(from);
const toPath = this._resolvePath(to);
return fs.rename(fromPath, toPath);
}
chmod(path, mode) {
path = nodePath.resolve(this.cwd, path);
path = this._resolvePath(path);
return fs.chmod(path, mode);
}

View File

@@ -1,4 +1,4 @@
module.exports = function (path) {
return path
.replace(/"/g, '""');
}
};

View File

@@ -1,21 +1,25 @@
const _ = require('lodash');
const format = require('date-fns/format');
const moment = require('moment');
const errors = require('../errors');
module.exports = function (fileStat, format = 'ls') {
if (typeof format === 'function') return format(fileStat);
const formats = {
'ls': ls,
'ep': ep
ls: ls,
ep: ep
};
if (!formats.hasOwnProperty(format)) {
throw new errors.FileSystemError('Bad file stat formatter');
}
return formats[format](fileStat);
}
};
function ls(fileStat) {
const now = moment.utc();
const mtime = moment.utc(new Date(fileStat.mtime));
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
return [
fileStat.mode !== null
? [
@@ -35,7 +39,7 @@ function ls(fileStat) {
fileStat.uid,
fileStat.gid,
_.padStart(fileStat.size, 12),
_.padStart(format(fileStat.mtime, 'MMM DD HH:mm'), 12),
_.padStart(mtime.format(dateFormat), 12),
fileStat.name
].join(' ');
}
@@ -44,7 +48,7 @@ function ep(fileStat) {
const facts = [
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
fileStat.size ? `s${fileStat.size}` : null,
fileStat.mtime ? `m${format(fileStat.mtime, 'X')}` : null,
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
fileStat.mode ? `up${fileStat.mode.toString(8).substr(fileStat.mode.toString(8).length - 3)}` : null,
fileStat.isDirectory() ? 'r' : '/'
].join(',');

View File

@@ -2,19 +2,19 @@ const net = require('net');
const when = require('when');
const errors = require('../errors');
module.exports = function (min = 22, max = undefined) {
module.exports = function (min = 1, max = undefined) {
return when.promise((resolve, reject) => {
let port = min;
let checkPort = min;
let portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
if (!max || port < max) {
port = port + 1;
portCheckServer.listen(port);
if (checkPort < 65535 && (!max || checkPort < max)) {
checkPort = checkPort + 1;
portCheckServer.listen(checkPort);
} else {
reject(new errors.GeneralError('Unable to find open port', 500));
}
})
});
portCheckServer.on('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => {
@@ -22,6 +22,6 @@ module.exports = function (min = 22, max = undefined) {
resolve(port);
});
});
portCheckServer.listen(port);
portCheckServer.listen(checkPort);
});
};

View File

@@ -21,5 +21,5 @@ module.exports = function (hostname) {
});
});
} else resolve(hostname);
})
}
});
};

View File

@@ -37,6 +37,8 @@ class FtpServer {
});
this.on = this.server.on.bind(this.server);
this.listeners = this.server.listeners.bind(this.server);
process.on('SIGINT', () => this.close());
}
listen() {
@@ -70,27 +72,28 @@ class FtpServer {
if (features.length) {
features.unshift('Features:');
features.push('.')
features.push('.');
}
return features.length ? features.join(' ') : 'Ready';
}
setGreeting(gretting) {
setGreeting(greeting) {
if (typeof greeting === 'string') {
this.options.greeting = greeting;
} else {
gretting.then(greeting => {
this.options.gretting = greeting;
})
greeting.then(greet => {
this.options.greeting = greet;
});
}
}
disconnectClient(id) {
return when.promise((resolve, reject) => {
return when.promise(resolve => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
return client.close(0);
client.close(0);
resolve();
});
}

View File

@@ -0,0 +1,53 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'ABOR';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve(),
connector: {
waitForConnection: () => when.resolve(),
end: () => when.resolve()
}
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.connector, 'waitForConnection');
sandbox.spy(mockClient.connector, 'end');
});
afterEach(() => {
sandbox.restore();
});
it('// successful | no active connection', done => {
mockClient.connector.waitForConnection.restore();
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
cmdFn()
.then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(0);
expect(mockClient.reply.args[0][0]).to.equal(226);
done();
})
.catch(done);
});
it('// successful | active connection', done => {
cmdFn()
.then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(1);
expect(mockClient.reply.args[0][0]).to.equal(426);
expect(mockClient.reply.args[1][0]).to.equal(226);
done();
})
.catch(done);
});
});

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'ALLO';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,11 +20,11 @@ describe(CMD, done => {
});
it('// successful', done => {
CMDFN()
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202)
expect(mockClient.reply.args[0][0]).to.equal(202);
done();
})
.catch(done);
})
});
});

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'AUTH';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,27 +20,27 @@ describe(CMD, done => {
});
it('TLS // not supported', done => {
CMDFN({command: {_: [CMD, 'TLS'], directive: CMD}})
cmdFn({command: {_: [CMD, 'TLS'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
it('SSL // not supported', done => {
CMDFN({command: {_: [CMD, 'SSL'], directive: CMD}})
cmdFn({command: {_: [CMD, 'SSL'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
it('bad // bad', done => {
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);

View File

@@ -4,7 +4,7 @@ const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CDUP';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
@@ -13,7 +13,7 @@ describe(CMD, done => {
chdir: () => when.resolve()
}
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -26,7 +26,7 @@ describe(CMD, done => {
});
it('.. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');

View File

@@ -1,23 +1,21 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'CWD';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { chdir: () => {} }
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'chdir').resolves();
});
afterEach(() => {
@@ -27,9 +25,9 @@ describe(CMD, done => {
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
@@ -39,9 +37,9 @@ describe(CMD, done => {
it('fails on no fs chdir command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
@@ -51,7 +49,7 @@ describe(CMD, done => {
});
it('test // successful', done => {
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
@@ -63,7 +61,7 @@ describe(CMD, done => {
it('test // successful', done => {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
@@ -74,9 +72,9 @@ describe(CMD, done => {
it('bad // unsuccessful', done => {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'))
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');

View File

@@ -1,18 +1,16 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'DELE';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { delete: () => {} }
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -27,9 +25,9 @@ describe(CMD, done => {
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
@@ -39,9 +37,9 @@ describe(CMD, done => {
it('fails on no fs delete command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
@@ -51,7 +49,7 @@ describe(CMD, done => {
});
it('test // successful', done => {
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
@@ -62,9 +60,9 @@ describe(CMD, done => {
it('bad // unsuccessful', done => {
mockClient.fs.delete.restore();
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'))
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'HELP';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,7 +20,7 @@ describe(CMD, done => {
});
it('// successful', done => {
CMDFN({command: {_: [CMD], directive: CMD}})
cmdFn({command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
done();
@@ -29,7 +29,7 @@ describe(CMD, done => {
});
it('help // successful', done => {
CMDFN({command: {_: [CMD, 'help'], directive: CMD}})
cmdFn({command: {_: [CMD, 'help'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
done();
@@ -38,7 +38,7 @@ describe(CMD, done => {
});
it('help // successful', done => {
CMDFN({command: {_: [CMD, 'allo'], directive: CMD}})
cmdFn({command: {_: [CMD, 'allo'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
done();
@@ -47,7 +47,7 @@ describe(CMD, done => {
});
it('bad // unsuccessful', done => {
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
done();

View File

@@ -2,17 +2,16 @@ const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised')(when.Promise);
const CMD = 'LIST';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { list: () => {} },
connector: {
waitForConnection: () => {},
waitForConnection: () => when({}),
end: () => {}
},
commandSocket: {
@@ -20,14 +19,12 @@ describe(CMD, done => {
pause: () => {}
}
};
const mockSocket = {};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
sandbox.stub(mockClient.fs, 'list').resolves([{
name: 'test1',
dev: 2114,
@@ -54,9 +51,9 @@ describe(CMD, done => {
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
@@ -66,9 +63,9 @@ describe(CMD, done => {
it('fails on no fs list command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
@@ -78,7 +75,7 @@ describe(CMD, done => {
});
it('. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
@@ -94,7 +91,7 @@ describe(CMD, done => {
mockClient.fs.list.restore();
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(451);
done();
@@ -103,10 +100,9 @@ describe(CMD, done => {
});
it('. // unsuccessful (timeout)', done => {
mockClient.connector.waitForConnection.restore();
sandbox.stub(mockClient.connector, 'waitForConnection').rejects(new when.TimeoutError());
sandbox.stub(mockClient.connector, 'waitForConnection').returns(when.reject(new when.TimeoutError()));
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();

View File

@@ -1,19 +1,16 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised')(when.Promise);
const CMD = 'MDTM';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { get: () => {} }
};
const mockSocket = {};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -28,9 +25,9 @@ describe(CMD, done => {
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
@@ -40,9 +37,9 @@ describe(CMD, done => {
it('fails on no fs get command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
@@ -52,7 +49,7 @@ describe(CMD, done => {
});
it('. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
@@ -65,7 +62,7 @@ describe(CMD, done => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();

View File

@@ -1,18 +1,16 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'MKD';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { mkdir: () => {} }
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -27,9 +25,9 @@ describe(CMD, done => {
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
@@ -39,9 +37,9 @@ describe(CMD, done => {
it('fails on no fs mkdir command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
@@ -51,7 +49,7 @@ describe(CMD, done => {
});
it('test // successful', done => {
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
@@ -63,7 +61,7 @@ describe(CMD, done => {
it('test // successful', done => {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').resolves('test');
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
@@ -74,9 +72,9 @@ describe(CMD, done => {
it('bad // unsuccessful', done => {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'))
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'));
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad');

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'MODE';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,18 +20,18 @@ describe(CMD, done => {
});
it('S // successful', done => {
CMDFN({command: {_: [CMD, 'S']}})
cmdFn({command: {_: [CMD, 'S']}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
it('Q // unsuccessful', done => {
CMDFN({command: {_: [CMD, 'Q']}})
cmdFn({command: {_: [CMD, 'Q']}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);

View File

@@ -2,17 +2,16 @@ const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised')(when.Promise);
const CMD = 'NLST';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { list: () => {} },
connector: {
waitForConnection: () => {},
waitForConnection: () => when({}),
end: () => {}
},
commandSocket: {
@@ -20,14 +19,12 @@ describe(CMD, done => {
pause: () => {}
}
};
const mockSocket = {};
const CMDFN = require(`../../src/commands/registration/list`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
sandbox.stub(mockClient.fs, 'list').resolves([{
name: 'test1',
dev: 2114,
@@ -52,7 +49,7 @@ describe(CMD, done => {
});
it('. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1][1]).to.have.property('raw');

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'NOOP';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,11 +20,11 @@ describe(CMD, done => {
});
it('// successful', done => {
CMDFN()
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
})
});
});

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'OPTS';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,11 +20,11 @@ describe(CMD, done => {
});
it('// successful', done => {
CMDFN()
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501)
expect(mockClient.reply.args[0][0]).to.equal(501);
done();
})
.catch(done);
})
});
});

View File

@@ -1,11 +1,9 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'PASS';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
@@ -14,7 +12,7 @@ describe(CMD, done => {
server: { options: { anonymous: false } },
username: 'user'
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -27,7 +25,7 @@ describe(CMD, done => {
});
it('pass // successful', done => {
CMDFN({log, command: {_: [CMD, 'pass'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'pass'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.args[0]).to.eql(['user', 'pass']);
@@ -39,7 +37,7 @@ describe(CMD, done => {
it('// successful (anonymous)', done => {
mockClient.server.options.anonymous = true;
mockClient.authenticated = true;
CMDFN({log, command: {_: [CMD], directive: CMD}})
cmdFn({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(0);
@@ -54,7 +52,7 @@ describe(CMD, done => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects('bad');
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
done();
@@ -64,9 +62,9 @@ describe(CMD, done => {
it('bad // unsuccessful', done => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects({})
sandbox.stub(mockClient, 'login').rejects({});
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
done();
@@ -76,7 +74,7 @@ describe(CMD, done => {
it('bad // unsuccessful', done => {
delete mockClient.username;
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
done();

View File

@@ -0,0 +1,54 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const ActiveConnector = require('../../src/connector/active');
const CMD = 'PORT';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful | no argument', done => {
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();
})
.catch(done);
});
it('// unsuccessful | invalid argument', done => {
cmdFn({ command: { _: [CMD, '1,2,3,4,5'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();
})
.catch(done);
});
it('// successful', done => {
cmdFn({ command: { _: [CMD, '192,168,0,100,137,214'] } })
.then(() => {
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('192.168.0.100');
expect(port).to.equal(35286);
done();
})
.catch(done);
});
});

View File

@@ -1,23 +1,21 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'PWD';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { currentDirectory: () => {} }
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
});
afterEach(() => {
@@ -27,9 +25,9 @@ describe(CMD, done => {
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
@@ -39,9 +37,9 @@ describe(CMD, done => {
it('fails on no fs currentDirectory command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
@@ -51,7 +49,7 @@ describe(CMD, done => {
});
it('// successful', done => {
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
done();
@@ -61,9 +59,9 @@ describe(CMD, done => {
it('// successful', done => {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test')
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test');
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'test'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
done();
@@ -73,9 +71,9 @@ describe(CMD, done => {
it('// unsuccessful', done => {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'))
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'));
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
cmdFn({log, command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();

View File

@@ -1,17 +1,13 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'QUIT';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
close: () => {}
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,7 +19,7 @@ describe(CMD, done => {
});
it('// successful', done => {
CMDFN()
cmdFn()
.then(() => {
expect(mockClient.close.callCount).to.equal(1);
done();

View File

@@ -0,0 +1,70 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'RNFR';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.renameFrom = 'test';
mockClient.fs = {
get: () => when.resolve()
};
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'get');
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
it('test // unsuccessful | file get fails', done => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('test // successful', done => {
cmdFn({ log: mockLog, command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.fs.get.args[0][0]).to.equal('test');
expect(mockClient.reply.args[0][0]).to.equal(350);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,82 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'RNTO';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.renameFrom = 'test';
mockClient.fs = {
get: () => when.resolve(),
rename: () => when.resolve()
};
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'rename');
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful | no renameFrom set', done => {
delete mockClient.renameFrom;
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
done();
})
.catch(done);
});
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
it('new // unsuccessful | rename fails', done => {
mockClient.fs.rename.restore();
sandbox.stub(mockClient.fs, 'rename').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { _: [CMD, 'new'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('new // successful', done => {
cmdFn({ command: { _: [CMD, 'new'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.rename.args[0]).to.eql(['test', 'new']);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,69 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CHMOD';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const cmdFn = require(`../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.fs = {
chmod: () => when.resolve()
};
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chmod');
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
it('777 test // unsuccessful | file chmod fails', done => {
mockClient.fs.chmod.restore();
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { _: [CMD, '777', 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
done();
})
.catch(done);
});
it('777 test // successful', done => {
cmdFn({ log: mockLog, command: { _: [CMD, '777', 'test'] } })
.then(() => {
expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]);
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,66 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'SIZE';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.fs = {
get: () => when.resolve({size: 1})
};
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
it('// unsuccessful | file get fails', done => {
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// successful', done => {
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
done();
})
.catch(done);
});
});

142
test/commands/stat.spec.js Normal file
View File

@@ -0,0 +1,142 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'STAT';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.fs = {
get: () => when.resolve({}),
list: () => when.resolve([])
};
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
done();
})
.catch(done);
});
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
it('// unsuccessful | file get fails', done => {
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(450);
done();
})
.catch(done);
});
it('// successful | file', done => {
sandbox.stub(mockClient.fs, 'get').returns({
name: 'test_file',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
});
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(212);
done();
})
.catch(done);
});
it('// successful | directory', done => {
sandbox.stub(mockClient.fs, 'list').returns([{
name: 'test_file',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
}]);
sandbox.stub(mockClient.fs, 'get').returns({
name: 'test_directory',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => true
});
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,83 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const stor = require('../../src/commands/registration/stor');
const CMD = 'STOU';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.fs = {
get: () => when.resolve(),
getUniqueName: () => when.resolve('4')
};
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'get');
sandbox.spy(mockClient.fs, 'getUniqueName');
sandbox.stub(stor.handler, 'call').resolves({});
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
it('// successful | given name is unique', done => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects({});
cmdFn({ command: { _: [CMD, 'good'] } })
.then(() => {
const call = stor.handler.call.args[0][1];
expect(call).to.have.property('command');
expect(call.command).to.have.property('_');
expect(call.command._).to.eql([CMD, 'good']);
expect(mockClient.fs.getUniqueName.callCount).to.equal(0);
done();
})
.catch(done);
});
it('// successful | generates unique name', done => {
cmdFn({ command: { _: [CMD, 'bad'] } })
.then(() => {
const call = stor.handler.call.args[0][1];
expect(call).to.have.property('command');
expect(call.command).to.have.property('_');
expect(call.command._).to.eql([CMD, '4']);
expect(mockClient.fs.getUniqueName.callCount).to.equal(1);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,39 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'STRU';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// successful', done => {
cmdFn({command: { _: [CMD, 'F'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
it('// unsuccessful', done => {
cmdFn({command: { _: [CMD, 'X'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,30 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'SYST';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(215);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,62 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'TYPE';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.encoding = null;
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('A // successful', done => {
cmdFn({ command: { _: [CMD, 'A'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.encoding).to.equal('utf-8');
done();
})
.catch(done);
});
it('I // successful', done => {
cmdFn({ command: { _: [CMD, 'I'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.encoding).to.equal('binary');
done();
})
.catch(done);
});
it('L // successful', done => {
cmdFn({ command: { _: [CMD, 'L'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.encoding).to.equal('binary');
done();
})
.catch(done);
});
it('X // successful', done => {
cmdFn({ command: { _: [CMD, 'X'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
expect(mockClient.encoding).to.equal(null);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,88 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'USER';
describe(CMD, function () {
let sandbox;
const mockLog = {
error: () => {}
};
const mockClient = {
reply: () => when.resolve(),
server: { options: {} },
login: () => when.resolve()
};
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
delete mockClient.username;
mockClient.server.options = {};
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient, 'login');
});
afterEach(() => {
sandbox.restore();
});
it('test // successful | prompt for password', done => {
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(331);
done();
})
.catch(done);
});
it('test // successful | anonymous login', done => {
mockClient.server.options = {anonymous: true};
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(1);
done();
})
.catch(done);
});
it('test // unsuccessful | no username provided', done => {
cmdFn({ command: { _: [CMD] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
expect(mockClient.login.callCount).to.equal(0);
done();
})
.catch(done);
});
it('test // unsuccessful | already set username', done => {
mockClient.username = 'test';
cmdFn({ command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
expect(mockClient.login.callCount).to.equal(0);
done();
})
.catch(done);
});
it('test // unsuccessful | login function rejects', done => {
mockClient.server.options = {anonymous: true};
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { _: [CMD, 'test'] } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
expect(mockClient.login.callCount).to.equal(1);
done();
})
.catch(done);
});
});

View File

@@ -0,0 +1,66 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const sinon = require('sinon');
const net = require('net');
const ActiveConnector = require('../../src/connector/active');
const findPort = require('../../src/helpers/find-port');
describe('Connector - Active //', function () {
let PORT;
let active;
let mockConnection = {};
let sandbox;
let server;
before(() => {
active = new ActiveConnector(mockConnection);
});
beforeEach(done => {
sandbox = sinon.sandbox.create();
findPort()
.then(port => {
PORT = port;
server = net.createServer()
.on('connection', socket => socket.destroy())
.listen(PORT, () => done());
});
});
afterEach(done => {
sandbox.restore();
server.close(done);
});
it('sets up a connection', function (done) {
active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
done();
})
.catch(done);
});
it('destroys existing connection, then sets up a connection', function (done) {
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(destroyFnSpy.callCount).to.equal(1);
expect(active.dataSocket).to.exist;
done();
})
.catch(done);
});
it('waits for connection', function (done) {
active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then(() => done())
.catch(done);
});
});

View File

@@ -0,0 +1,116 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const sinon = require('sinon');
const when = require('when');
const net = require('net');
const bunyan = require('bunyan');
const PassiveConnector = require('../../src/connector/passive');
describe('Connector - Passive //', function () {
let passive;
let mockConnection = {
reply: () => when.resolve({}),
close: () => when.resolve({}),
encoding: 'utf8',
log: bunyan.createLogger({name: 'passive-test'}),
commandSocket: {},
server: { options: {} }
};
let sandbox;
before(() => {
passive = new PassiveConnector(mockConnection);
});
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockConnection, 'reply');
sandbox.spy(mockConnection, 'close');
mockConnection.commandSocket.remoteAddress = '::ffff:127.0.0.1';
mockConnection.server.options.pasv_range = '8000';
});
afterEach(() => {
sandbox.restore();
});
it('cannot wait for connection with no server', function (done) {
passive.waitForConnection()
.then(() => done('should not happen'))
.catch(err => {
expect(err.name).to.equal('ConnectorError');
done();
});
});
it('has invalid pasv range', function (done) {
mockConnection.server.options.pasv_range = -1;
passive.setupServer()
.then(() => done('should not happen'))
.catch(err => {
expect(err.name).to.equal('RangeError');
done();
});
});
it('sets up a server', function (done) {
passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
done();
})
.catch(done);
});
it('destroys existing server, then sets up a server', function (done) {
const closeFnSpy = sandbox.spy(passive.dataServer, 'close');
passive.setupServer()
.then(() => {
expect(closeFnSpy.callCount).to.equal(1);
expect(passive.dataServer).to.exist;
done();
})
.catch(done);
});
it('refuses connection with different remote address', function (done) {
mockConnection.commandSocket.remoteAddress = 'bad';
passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
const {port} = passive.dataServer.address();
net.createConnection(port);
passive.dataServer.once('connection', () => {
setTimeout(() => {
expect(passive.connection.reply.callCount).to.equal(1);
expect(passive.connection.reply.args[0][0]).to.equal(550);
done();
}, 100);
});
})
.catch(done);
});
it('accepts connection', function (done) {
passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
const {port} = passive.dataServer.address();
net.createConnection(port);
return passive.waitForConnection();
})
.then(() => {
expect(passive.dataSocket).to.exist;
passive.end();
done();
})
.catch(done);
});
});

View File

@@ -1,8 +1,9 @@
const {expect} = require('chai');
const fileStat = require('../../src/helpers/file-stat');
const errors = require('../../src/errors');
describe.skip('helpers // file-stat', function () {
describe('helpers // file-stat', function () {
const STAT = {
name: 'test1',
dev: 2114,
@@ -15,24 +16,48 @@ describe.skip('helpers // file-stat', function () {
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
atime: 'Mon, 10 Oct 2017 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2017 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2017 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2017 23:24:11 GMT',
isDirectory: () => false
};
const STAT_OLD = {
name: 'test2',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 84,
gid: 101,
rdev: 0,
size: 530,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 14:05:12 GMT',
mtime: 'Mon, 10 Oct 2011 14:05:12 GMT',
ctime: 'Mon, 10 Oct 2011 14:05:12 GMT',
birthtime: 'Mon, 10 Oct 2011 14:05:12 GMT',
isDirectory: () => false
};
describe('format - ls //', function () {
it('formats correctly', () => {
const format = fileStat(STAT, 'ls');
expect(format).to.equal('-rwxrw-r-- 1 85 100 527 Oct 10 17:24 test1');
expect(format).to.equal('-rwxrw-r-- 1 85 100 527 Oct 10 23:24 test1');
});
it('formats correctly for files over 6 months old', () => {
const format = fileStat(STAT_OLD, 'ls');
expect(format).to.equal('-rwxrw-r-- 1 84 101 530 Oct 10 2011 test2');
});
});
describe('format - ep //', function () {
it('formats correctly', () => {
const format = fileStat(STAT, 'ep');
expect(format).to.equal('+i842.2dd69c9,s527,m1318289051,up644,/ test1');
expect(format).to.equal('+i842.2dd69c9,s527,m1507677851,up644,/ test1');
});
});
@@ -43,11 +68,11 @@ describe.skip('helpers // file-stat', function () {
});
it('formats correctly', () => {
function customerFormater(fileStat) {
return [fileStat.gid, fileStat.name, fileStat.size].join('\t');
function customerFormater(stat) {
return [stat.gid, stat.name, stat.size].join('\t');
}
const format = fileStat(STAT, customerFormater);
expect(format).to.equal('100\ttest1\t527');
})
});
});
});

View File

@@ -1,3 +1,4 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const {Server} = require('net');
const sinon = require('sinon');
@@ -10,7 +11,7 @@ describe('helpers // find-port', function () {
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(Server.prototype, 'listen')
sandbox.spy(Server.prototype, 'listen');
});
afterEach(() => {
sandbox.restore();
@@ -30,6 +31,7 @@ describe('helpers // find-port', function () {
findPort(1, 2)
.then(() => done('no'))
.catch(err => {
expect(err).to.exist;
done();
});
});

View File

@@ -1,16 +1,17 @@
require('dotenv').load();
const _ = require('lodash');
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const bunyan = require('bunyan');
const fs = require('fs');
const sinon = require('sinon');
const FtpServer = require('../src');
const FtpClient = require('ftp');
before(() => require('dotenv').load());
describe('FtpServer', function () {
this.timeout(2000);
let log = bunyan.createLogger({name: 'test', level: 10});
let log = bunyan.createLogger({name: 'test'});
log.level(process.env.LOG_LEVEL || 'debug');
let server;
let client;
@@ -19,17 +20,12 @@ describe('FtpServer', function () {
log,
pasv_range: process.env.PASV_RANGE
});
server.on('login', (data, resolve, reject) => {
resolve();
});
process.on('SIGINT', function() {
server.close();
server.on('login', (data, resolve) => {
resolve({root: process.cwd()});
});
require('child_process').exec(`sudo kill $(sudo lsof -t -i:${server.url.port})`, () => {
server.listen()
.finally(() => done());
});
server.listen()
.then(() => done());
});
after(() => {
server.close();
@@ -39,6 +35,7 @@ describe('FtpServer', function () {
expect(server).to.exist;
client = new FtpClient();
client.once('ready', () => done());
client.once('error', err => done(err));
client.connect({
host: server.url.hostname,
port: server.url.port,
@@ -63,8 +60,17 @@ describe('FtpServer', function () {
});
});
it('CWD process.cwd()', done => {
const dir = require('path').resolve(process.cwd(), 'test');
it('CWD ..', done => {
const dir = '..';
client.cwd(`${dir}`, (err, data) => {
expect(err).to.not.exist;
expect(data).to.be.a('string');
done();
});
});
it('CWD test', done => {
const dir = 'test';
client.cwd(`${dir}`, (err, data) => {
expect(err).to.not.exist;
expect(data).to.be.a('string');
@@ -100,7 +106,7 @@ describe('FtpServer', function () {
done();
});
});
})
});
it('APPE test.txt', done => {
const buffer = Buffer.from(', awesome!');
@@ -150,14 +156,14 @@ describe('FtpServer', function () {
});
it('MDTM awesome.txt', done => {
client.lastMod('awesome.txt', (err, date) => {
client.lastMod('awesome.txt', err => {
expect(err).to.not.exist;
done();
});
});
it('SITE CHMOD 700 awesome.txt', done => {
client.site('CHMOD 600 awesome.txt', (err) => {
client.site('CHMOD 600 awesome.txt', err => {
expect(err).to.not.exist;
fs.stat('./test/awesome.txt', (fserr, stats) => {
expect(fserr).to.not.exist;
@@ -175,7 +181,7 @@ describe('FtpServer', function () {
done();
});
});
}
};
it('TYPE A', done => {
client.ascii(err => {
@@ -227,4 +233,10 @@ describe('FtpServer', function () {
});
});
it('QUIT', done => {
client.once('close', done);
client.logout(err => {
expect(err).to.be.undefined;
});
});
});

View File

@@ -1,16 +0,0 @@
require('dotenv').load();
const bunyan = require('bunyan');
const FtpServer = require('../src');
const log = bunyan.createLogger({name: 'test'});
log.level(process.env.LOG_LEVEL || 'trace');
const server = new FtpServer(process.env.FTP_URL, {
log,
pasv_range: process.env.PASV_RANGE
});
server.on('login', ({username, password}, resolve, reject) => {
if (username === 'test' && password === 'test') resolve();
else reject('Bad username or password');
});
server.listen();