Compare commits

..

58 Commits

Author SHA1 Message Date
Tyler Stewart
1ba67034b1 chore(README): fix class name references 2017-10-31 16:18:36 -06:00
Tyler Stewart
0a331c5998 fix(package): correct main file
This was changed with the confit update
2017-10-31 16:18:36 -06:00
Tyler Stewart
a7103ded7e fix: add values to resolved promises 2017-10-30 18:48:42 -06:00
Tyler Stewart
d787d4cab6 test(site): test site registry call 2017-10-30 17:52:05 -06:00
Tyler Stewart
154cd5a5d7 chore: eslint --fix 2017-10-30 17:52:05 -06:00
Tyler Stewart
5fc59b50b1 chore: update package versions 2017-10-30 17:52:05 -06:00
Tyler Stewart
043c97c80f chore: update build with confit 2017-10-30 17:52:05 -06:00
Tyler Stewart
772fe5ca06 feat: add eprt and epsv for IPv6 support 2017-10-30 17:52:05 -06:00
Ozair Patel
e272802525 feat(typings): swapped declare class for export interface 2017-10-25 21:46:24 -06:00
Ozair Patel
7589322abc feat(typings): removed Server extension for FtpServer 2017-10-25 21:46:24 -06:00
Ozair Patel
fae5564041 feat(typings): removed typed dependency and fixed import error 2017-10-25 21:46:24 -06:00
Ozair Patel
e9b4a6385d feat(typings): ypdated typescript typings 2017-10-25 21:46:24 -06:00
Tyler Stewart
71621aae4f Merge pull request #44 from trs/include-types-file-on-install
fix(package): include types file
2017-10-25 12:22:57 -06:00
Tyler Stewart
0eaa0f8743 chore(travis): only test v6
Node 8 fails for some reason, will look into in the future
2017-10-25 12:18:14 -06:00
Tyler Stewart
8828a4ea09 fix(package): include types file
Should ensure types are inluded on an install
2017-10-25 12:00:20 -06:00
Tyler Stewart
b33659320f Merge pull request #40 from trs/fix-socket-ref
fix(retr): check for connector socket
2017-09-30 11:14:31 -06:00
Tyler Stewart
6a6b949d3b fix(retr): check for connector socket
Ensures socket still exists and client hasn't disconnected
2017-09-30 11:09:30 -06:00
Tyler Stewart
283be85db3 Merge pull request #38 from trs/improve-connector-stream-handling
Improve connector stream handling
2017-08-18 12:45:32 -06:00
Tyler Stewart
e555ce9230 test(stor): add failure test 2017-08-18 12:06:58 -06:00
Tyler Stewart
e6575808f1 fix(stor): improve event and promise handling 2017-08-18 12:06:42 -06:00
Tyler Stewart
a5e58a106e fix(retr): improve event and promise handling 2017-08-18 12:06:42 -06:00
Tyler Stewart
ed086e576a Merge pull request #36 from trs/fix-process-exit
Fix process exit
2017-08-14 17:07:05 -06:00
Tyler Stewart
31f0f3b0dc fix: ensure process exits 2017-08-14 16:46:11 -06:00
Tyler Stewart
d763820c86 refactor: connector socket getter 2017-08-14 16:45:37 -06:00
Tyler Stewart
f3183314cc Merge pull request #34 from trs/add-typings
feat(typings): add TypeScript .d.ts files
2017-07-24 11:00:13 -06:00
Chris Rabl
dde7b36c46 feat(typings): add TypeScript .d.ts files
* created outline for TypeScript declarations
* added basic object shapes for FtpServer and FileSystem
2017-07-24 10:53:41 -06:00
Tyler Stewart
00af9e7e61 Merge pull request #32 from trs/command-args
Command args
2017-07-10 10:02:55 -06:00
Tyler Stewart
99a885cd44 test(commands): update parser tests 2017-07-10 09:58:24 -06:00
Tyler Stewart
443051d753 fix(commands): get flags from ftp command 2017-07-07 17:28:09 -06:00
Tyler Stewart
27ecc4d835 fix(feat): order features alphabetically 2017-07-06 17:51:06 -06:00
Tyler Stewart
c8526be1f4 Merge pull request #30 from trs/fix-utf8
Fix utf8
2017-06-26 19:54:51 -06:00
Tyler Stewart
e0b11ff480 fix: cleanup server 2017-06-26 16:39:00 -06:00
Tyler Stewart
58b9d8db9d chore: update fs readme 2017-06-26 16:39:00 -06:00
Tyler Stewart
fa121ba0fd test(REST): add tests 2017-06-26 16:39:00 -06:00
Tyler Stewart
2e02dc20ad feat(REST): add support for REST command
Allows the client to resume a transfer at the specified bytes
2017-06-26 16:38:59 -06:00
Tyler Stewart
8aeb6976d2 fix(auth): update checks, ensure secure is set using ftps 2017-06-26 16:38:59 -06:00
Tyler Stewart
84a68ae03c chore: dont append branch name to commit 2017-06-26 12:34:21 -06:00
Tyler Stewart
9dfc80b99d test(OPTS): update tests
master
2017-06-26 11:19:42 -06:00
Tyler Stewart
090e3d8105 chore: update log levels 2017-06-26 11:13:55 -06:00
Tyler Stewart
c3b0dbf5b0 feat(OPTS): add opts command handler for utf8
master
2017-06-26 11:13:49 -06:00
Tyler Stewart
69a5133936 fix(FEAT): correctly display feature list 2017-06-26 11:13:00 -06:00
Tyler Stewart
5394908a6b Merge pull request #29 from trs/fix-tls-check
Fix encoding/transfer type
2017-06-20 17:20:23 -06:00
Tyler Stewart
3e7bd5bcf9 chore: update licence format
Still MIT, just updating to ensure GitHub recognizes it
2017-06-20 17:09:49 -06:00
Tyler Stewart
175b422c5f chore: update dependencies 2017-06-20 17:02:05 -06:00
Tyler Stewart
b2a9851204 fix: ensure utf8 support; allows accent characters 2017-06-20 16:56:26 -06:00
Tyler Stewart
977dd1579a fix(TYPE): correctly set types, only use for data connections 2017-06-20 16:55:37 -06:00
Tyler Stewart
176b2b7ca8 fix: actually check _tls 2017-06-20 14:47:44 -06:00
Tyler Stewart
63777c0d74 Merge pull request #26 from trs/test-updates
Test updates
2017-06-16 15:17:31 -06:00
Tyler Stewart
9be8ffa60d test: add and update tests 2017-06-09 15:00:01 -06:00
Tyler Stewart
b8cd6022e1 fix: assign tls options to empty object 2017-06-09 14:40:19 -06:00
Tyler Stewart
0618a3c675 fix(passive): throw error on invalid port range 2017-06-09 14:39:21 -06:00
Tyler Stewart
3c533a5fbc Merge pull request #25 from trs/export-fs
Export fs
2017-06-09 10:31:42 -06:00
Tyler Stewart
3d0a58ca15 docs(readme): update file system override details
master

export-fs

export-fs
2017-06-09 10:28:03 -06:00
Tyler Stewart
4b4c809af8 feat: export FileSystem and FtpSrv
Non breaking, as it still exports FtpSrv by default
2017-06-09 10:28:03 -06:00
Tyler Stewart
a234534de0 Merge pull request #23 from trs/write-stream-close
feat(fs): close stream before ending
2017-06-08 17:35:58 -06:00
Tyler Stewart
635fb35341 fix(stor): end stream if no close listener present 2017-06-08 17:31:02 -06:00
Tyler Stewart
51a6448ac2 fix(stor): ensure stream is destroyed once used up 2017-06-08 17:23:14 -06:00
Tyler Stewart
4d8a69615c feat(fs): close stream before ending
This allows processing of the received data before a response is sent to the client.

write-stream-close
2017-06-08 17:17:09 -06:00
84 changed files with 1510 additions and 5203 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ node_modules/
dist/
reports/
npm-debug.log
.nyc_output

24
.nycrc Normal file
View File

@@ -0,0 +1,24 @@
{
"check-coverage": true,
"per-file": true,
"lines": 40,
"statements": 40,
"functions": 20,
"branches": 40,
"include": [
"src/**/*.js"
],
"exclude": [
"test/**/*.spec.js"
],
"reporter": [
"lcovonly",
"html",
"text",
"cobertura",
"json"
],
"cache": true,
"all": true,
"report-dir": "./reports/coverage/"
}

View File

@@ -1,7 +1,7 @@
language: node_js
node_js:
- "6"
- "node"
# - "node"
env:
FTP_URL: ftp://127.0.0.1:8880

View File

@@ -136,6 +136,9 @@ Command | Description
Command | Description
:------ | :----------
<pre>npm run verify</pre> | Verify code style and syntax<ul><li>Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)</li></ul>
<pre>npm run verify:js</pre> | Verify Javascript code style and syntax
<pre>npm run verify:js:fix</pre> | Verify Javascript code style and syntax and fix any errors that can be fixed automatically
<pre>npm run verify:js:watch</pre> | Verify Javascript code style and syntax and watch files for changes
<pre>npm run verify:watch</pre> | Runs verify task whenever JS or CSS code is changed

22
LICENSE
View File

@@ -1,9 +1,21 @@
ftp-srv Copyright (c) 2017 Tyler Stewart
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (c) 2017 Tyler Stewart
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -37,8 +37,8 @@
```js
// Quick start
const FtpSvr = require('ftp-srv');
const ftpServer = new FtpSvr('ftp://0.0.0.0:9876', { options ... });
const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
@@ -105,7 +105,7 @@ A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by de
## Events
The `FtpSvr` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
### `login`
```js
@@ -150,11 +150,26 @@ Occurs when an error arises in the client connection.
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
## File System
The default file system can be overwritten to use your own implementation.
The default [file system](src/fs.js) can be overwritten to use your own implementation.
This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user.
Custom file systems can implement the following variables depending on the developers needs.
The default file system is exported and can be extended as needed:
```js
const {FtpSrv, FileSystem} = require('ftp-srv');
class MyFileSystem extends FileSystem {
constructor() {
super(...arguments);
}
get(fileName) {
...
}
}
```
Custom file systems can implement the following variables depending on the developers needs:
### Methods
#### [`currentDirectory()`](src/fs.js#L29)
@@ -177,13 +192,17 @@ __Used in:__ `CWD`, `CDUP`
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append = false})`](src/fs.js#L68)
#### [`write(fileName, {append, start})`](src/fs.js#L68)
Returns a writable stream
Options: `append` if true, append to existing file
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName)`](src/fs.js#L75)
#### [`read(fileName, {start})`](src/fs.js#L75)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L87)
@@ -191,11 +210,11 @@ Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L102)
Rename a file or directory
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L108)
Modify a file or directory's permissions
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L113)

View File

@@ -15,12 +15,7 @@ module.exports = {
{value: 'WIP', name: 'WIP: Work in progress'}
],
scopes: [
{name: 'accounts'},
{name: 'admin'},
{name: 'exampleScope'},
{name: 'changeMe'}
],
scopes: [],
// it needs to match the value for field type. Eg.: 'fix'
/*
@@ -39,5 +34,5 @@ module.exports = {
allowBreakingChanges: ['feat', 'fix'],
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: true
appendBranchNameToCommitMessage: false
};

View File

@@ -1,35 +0,0 @@
// Use JS to support loading of threshold data from external file
var coverageConfig = {
instrumentation: {
root: 'src/',
excludes: ['errors.js']
},
check: require('./thresholds.json'),
reporting: {
print: 'both',
dir: 'reports/coverage/',
reports: [
'cobertura',
'html',
'lcovonly',
'html',
'json'
],
'report-config': {
cobertura: {
file: 'cobertura/coverage.xml'
},
json: {
file: 'json/coverage.json'
},
lcovonly: {
file: 'lcov/lcov.info'
},
text: {
file: null
}
}
}
};
module.exports = coverageConfig;

View File

@@ -1,3 +1,5 @@
test/**/*.spec.js
--reporter mocha-pretty-bunyan-nyan
--reporter mocha-multi-reporters
--reporter-options configFile=config/testUnit/reporters.json
--no-timeouts
--ui bdd

View File

@@ -0,0 +1,6 @@
{
"reporterEnabled": "list, mocha-junit-reporter",
"mochaJunitReporterReporterOptions": {
"mochaFile": "reports/junit.xml"
}
}

View File

@@ -1,14 +0,0 @@
{
"global": {
"statements": 90,
"branches": 80,
"functions": 90,
"lines": 90
},
"each": {
"statements": 70,
"branches": 40,
"functions": 60,
"lines": 70
}
}

View File

@@ -1,60 +1,162 @@
# START_CONFIT_GENERATED_CONTENT
confit:
extends: &confit-extends
- plugin:node/recommended
plugins: &confit-plugins
- node
env: &confit-env
commonjs: true # For Webpack, CommonJS
node: true
mocha: true
es6: true
globals: &confit-globals {}
parser: &confit-parser espree
parserOptions: &confit-parserOptions
ecmaVersion: 6
sourceType: module
ecmaFeatures:
globalReturn: false
impliedStrict: true
jsx: false
# END_CONFIT_GENERATED_CONTENT
# Customise this section to meet your needs...
extends: *confit-extends
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-extends" from the line above
# <<: *confit-extends
plugins: *confit-plugins
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-plugins" from the line above
# <<: *confit-extends
env:
<<: *confit-env
globals:
<<: *confit-globals
parser: *confit-parser
parserOptions:
<<: *confit-parserOptions
rules:
max-len:
- warn
- 200 # Line Length
node/no-unpublished-require:
- 2
- allowModules:
- chai
- dotenv
- ftp
- sinon
- sinon-as-promised
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"mocha",
"node"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"no-warning-comments": [
1,
{
"terms": ["todo", "fixme", "xxx"],
"location": "start"
},
],
"object-curly-spacing": [
2,
"never"
],
"array-bracket-spacing": [
2,
"never"
],
"brace-style": [
2,
"1tbs"
],
"consistent-return": 0,
"indent": [
"error",
2,
{
"SwitchCase": 1,
"MemberExpression": "off"
}
],
"no-multiple-empty-lines": [
2,
{
"max": 2
}
],
"no-use-before-define": [
2,
"nofunc"
],
"one-var": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single"
],
"keyword-spacing": 2,
"space-before-function-paren": [
2,
{
"anonymous": "always",
"named": "never"
}
],
"space-in-parens": [
2,
"never"
],
"strict": [
2,
"global"
],
"curly": [
2,
"multi-line"
],
"eol-last": 2,
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"no-eval": 2,
"no-with": 2,
"space-infix-ops": 2,
"dot-notation": [
2,
{
"allowKeywords": true
}
],
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"yoda": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-undef-init": 2,
"no-console": 1,
"camelcase": [
0,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "as-needed"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-fallthrough": 0,
"semi": 2,
"semi-spacing": [
2,
{
"before": false,
"after": true
}
]
},
"parserOptions": {
"emcaVersion": 6,
"sourceType": "module",
"impliedStrict": true
}
}

View File

@@ -1,6 +1,6 @@
generator-confit:
app:
_version: f02196cc5cb7941ca46ec46d23bd6aef0dfcaca0
_version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba
buildProfile: Latest
copyrightOwner: Tyler Stewart
license: MIT
@@ -8,7 +8,7 @@ generator-confit:
publicRepository: true
repositoryType: GitHub
paths:
_version: 7f33e41600b34cd6867478d8f2b3d6b2bbd42508
_version: 780b129e0c7e5cab7e29c4f185bcf78524593a33
config:
configDir: config/
input:
@@ -18,22 +18,23 @@ generator-confit:
prodDir: dist/
reportDir: reports/
buildJS:
_version: df428a706d926204228c5d9ebdbd7b49908926d9
_version: ead8ce4280b07d696aff499a5fca1a933727582f
framework: []
frameworkScripts: []
outputFormat: ES6
sourceFormat: ES6
entryPoint:
_version: de20402bf85c703080ef6daf21e35325a3b9d604
_version: 39082c3df887fbc08744dfd088c25465e7a2e3a4
entryPoints:
main:
- src/index.js
testUnit:
_version: 4472a6d59b434226f463992d3c1914c77a6a115d
_version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a
testDependencies: []
testFramework: mocha
verify:
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
jsCodingStandard: eslint
jsCodingStandard: none
documentation:
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
generateDocs: false

111
ftp-srv.d.ts vendored Normal file
View File

@@ -0,0 +1,111 @@
import * as tls from 'tls'
import { Stats } from 'fs'
export interface FileSystem {
constructor(connection: any, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string;
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>;
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
}
export interface FtpConnection {
server: FtpServer;
id: string;
log: any;
transferType: string;
encoding: string;
bufferSize: boolean;
readonly ip: string;
restByteCount: number | undefined;
secure: boolean
close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
reply (options: number | Object, ...letters: Array<any>): Promise<any>
}
export interface FtpServerOptions {
pasv_range?: number | string,
greeting?: string,
tls?: tls.SecureContext | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log: any
}
export interface FtpServer {
constructor(url: string, options?: FtpServerOptions);
readonly isTLS: boolean;
listen(): any;
emitPromise(action: any, ...data: any[]): Promise<any>;
emit(action: any, ...data: any[]): void;
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string
};
setupGreeting(greet: string): string[];
setupFeaturesMessage(): string;
disconnectClient(id: string): Promise<any>;
close(): any;
on(event: "login", listener: (
data: {
connection: FtpConnection,
username: string,
password: string
},
resolve: (fs?: FileSystem, root?: string, cwd?: string, blacklist?: Array<string>, whitelist?: Array<string>) => void,
reject: (err?: Error) => void
) => void)
on(event: "client-error", listener: (
data: {
connection: FtpConnection,
context: string,
error: Error,
}
) => void)
}
declare const FtpSrv: FtpServer;
export default FtpServer;

6
ftp-srv.js Normal file
View File

@@ -0,0 +1,6 @@
const FtpSrv = require('./src');
const FileSystem = require('./src/fs');
module.exports = FtpSrv;
module.exports.FtpSrv = FtpSrv;
module.exports.FileSystem = FileSystem;

4330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,13 +8,16 @@
"ftp-srv",
"ftp-svr",
"ftpd",
"ftpserver",
"server"
],
"license": "MIT",
"main": "src/index.js",
"files": [
"src"
"src",
"ftp-srv.d.ts"
],
"main": "ftp-srv.js",
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/trs/ftp-srv"
@@ -29,11 +32,11 @@
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
"start": "npm run dev",
"test": "npm run test:unit",
"test:check-coverage": "cross-env NODE_ENV=test istanbul check-coverage reports/coverage/coverage.json --config config/testUnit/istanbul.js",
"test:check-coverage": "nyc check-coverage",
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
"test:unit": "chokidar 'src/**/*.js' 'test/**/*.js' -c 'npm run test:unit:once' --initial --silent",
"test:unit:once": "cross-env NODE_ENV=test istanbul cover --config config/testUnit/istanbul.js _mocha -- --opts config/testUnit/mocha.opts",
"upload-coverage": "cat reports/coverage/lcov/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w",
"test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts",
"upload-coverage": "cat reports/coverage/lcov.info | coveralls",
"verify": "npm run verify:js --silent",
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
@@ -52,30 +55,35 @@
"bunyan": "^1.8.10",
"lodash": "^4.17.4",
"moment": "^2.18.1",
"uuid": "^3.0.1",
"uuid": "^3.1.0",
"when": "^3.7.8"
},
"devDependencies": {
"chai": "^4.0.2",
"chokidar-cli": "1.2.0",
"coveralls": "2.13.1",
"cross-env": "5.0.1",
"cz-customizable": "5.0.0",
"cross-env": "3.1.4",
"cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "3.19.0",
"eslint": "4.5.0",
"eslint-config-google": "0.8.0",
"eslint-plugin-node": "5.0.0",
"eslint-friendly-formatter": "3.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "5.1.1",
"ftp": "^0.3.10",
"html-convert": "^2.1.7",
"husky": "0.13.4",
"husky": "0.13.3",
"istanbul": "0.4.5",
"mocha": "3.4.2",
"mocha": "3.5.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"mocha-pretty-bunyan-nyan": "^1.0.4",
"npm-run-all": "4.0.2",
"nyc": "11.1.0",
"rimraf": "2.6.1",
"semantic-release": "^6.3.6",
"sinon": "^2.3.2"
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x",

View File

@@ -12,10 +12,18 @@ class FtpCommands {
}
parse(message) {
const [directive, ...args] = message.replace(/"/g, '').split(' ');
const strippedMessage = message.replace(/"/g, '');
const [directive, ...args] = strippedMessage.split(' ');
const params = args.reduce(({arg, flags}, param) => {
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive: _.chain(directive).trim().toUpper().value(),
arg: _.compact(args).join(' ') || null,
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
};
return command;
@@ -54,7 +62,7 @@ class FtpCommands {
}
const handler = commandRegister.handler.bind(this.connection);
return when.try(handler, { log, command, previous_command: this.previousCommand })
return when.try(handler, {log, command, previous_command: this.previousCommand})
.finally(() => {
this.previousCommand = _.clone(command);
});

View File

@@ -20,7 +20,8 @@ module.exports = {
};
function handleTLS() {
if (!this.server._tls) return this.reply(504);
if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202);
return this.reply(234)
.then(() => {

View File

@@ -9,11 +9,11 @@ const FAMILY = {
module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const [protocol, ip, port] = _.compact(command.arg.split('|'));
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(502, 'Unknown network protocol');
if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
},

View File

@@ -9,8 +9,12 @@ module.exports = {
const feat = _.get(registry[cmd], 'flags.feat', null);
if (feat) return _.concat(feats, feat);
return feats;
}, [])
.map(feat => ` ${feat}`);
}, ['UTF8'])
.sort()
.map(feat => ({
message: ` ${feat}`,
raw: true
}));
return features.length
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features');

View File

@@ -13,13 +13,9 @@ module.exports = {
const simple = command.directive === 'NLST';
let dataSocket;
const path = command.arg || '.';
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.get.bind(this.fs), path))
.then(stat => stat.isDirectory() ? when.try(this.fs.list.bind(this.fs), path) : [stat])
.then(files => {
@@ -33,7 +29,7 @@ module.exports = {
return {
raw: true,
message,
socket: dataSocket
socket: this.connector.socket
};
});
return this.reply(150)

View File

@@ -1,8 +1,34 @@
const _ = require('lodash');
const OPTIONS = {
UTF8: utf8,
'UTF-8': utf8
};
module.exports = {
directive: 'OPTS',
handler: function () {
return this.reply(501);
handler: function ({command} = {}) {
if (!_.has(command, 'arg')) return this.reply(501);
const [_option, ...args] = command.arg.split(' ');
const option = _.toUpper(_option);
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
return OPTIONS[option].call(this, args);
},
syntax: '{{cmd}}',
description: 'Select options for a feature'
};
function utf8([setting] = []) {
switch (_.toUpper(setting)) {
case 'ON':
this.encoding = 'utf8';
return this.reply(200, 'UTF8 encoding on');
case 'OFF':
this.encoding = 'ascii';
return this.reply(200, 'UTF8 encoding off');
default:
return this.reply(501, 'Unknown setting for option');
}
}

View File

@@ -8,6 +8,7 @@ module.exports = {
syntax: '{{cmd}}',
description: 'Protection Buffer Size',
flags: {
no_auth: true
no_auth: true,
feat: 'PBSZ'
}
};

View File

@@ -5,6 +5,7 @@ module.exports = {
directive: 'PORT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);

View File

@@ -17,6 +17,7 @@ module.exports = {
syntax: '{{cmd}}',
description: 'Data Channel Protection Level',
flags: {
no_auth: true
no_auth: true,
feat: 'PROT'
}
};

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'QUIT',
handler: function () {
return this.close(221);
return this.close(221, 'Client called QUIT');
},
syntax: '{{cmd}}',
description: 'Disconnect',

View File

@@ -0,0 +1,16 @@
const _ = require('lodash');
module.exports = {
directive: 'REST',
handler: function ({command} = {}) {
const arg = _.get(command, 'arg');
const byteCount = parseInt(arg, 10);
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
this.restByteCount = byteCount;
return this.reply(350, `Resarting next transfer at ${byteCount}`);
},
syntax: '{{cmd}} <byte-count>',
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
};

View File

@@ -6,23 +6,26 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when.try(this.fs.read.bind(this.fs), command.arg))
.tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.read.bind(this.fs), command.arg, {start: this.restByteCount}))
.then(stream => {
return when.promise((resolve, reject) => {
dataSocket.on('error', err => stream.emit('error', err));
this.restByteCount = 0;
stream.on('data', data => dataSocket.write(data, this.encoding));
stream.on('end', () => resolve(this.reply(226)));
stream.on('error', err => reject(err));
this.reply(150).then(() => dataSocket.resume());
const eventsPromise = when.promise((resolve, reject) => {
this.connector.socket.once('error', err => reject(err));
stream.on('data', data => this.connector.socket
&& this.connector.socket.write(data, this.transferType));
stream.once('error', err => reject(err));
stream.once('end', () => resolve());
});
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => eventsPromise)
.finally(() => stream.destroy ? stream.destroy() : null);
})
.then(() => this.reply(226))
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');

View File

@@ -1,16 +1,19 @@
const when = require('when');
const _ = require('lodash');
const registry = require('./registry');
module.exports = {
directive: 'SITE',
handler: function ({log, command} = {}) {
const registry = require('./registry');
const subCommand = this.commands.parse(command.arg);
const rawSubCommand = _.get(command, 'arg', '');
const subCommand = this.commands.parse(rawSubCommand);
const subLog = log.child({subverb: subCommand.directive});
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
const handler = registry[subCommand.directive].handler.bind(this);
return when.try(handler, { log: subLog, command: subCommand });
return when.try(handler, {log: subLog, command: subCommand});
},
syntax: '{{cmd}} <subVerb> [...<subParams>]',
description: 'Sends site specific commands to remote server'

View File

@@ -9,23 +9,32 @@ module.exports = {
const append = command.directive === 'APPE';
const fileName = command.arg;
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append}))
.tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append, start: this.restByteCount}))
.then(stream => {
return when.promise((resolve, reject) => {
stream.on('error', err => dataSocket.emit('error', err));
this.restByteCount = 0;
dataSocket.on('end', () => stream.end(() => resolve(this.reply(226, fileName))));
dataSocket.on('error', err => reject(err));
dataSocket.on('data', data => stream.write(data, this.encoding));
this.reply(150).then(() => dataSocket.resume());
const streamPromise = when.promise((resolve, reject) => {
stream.once('error', err => reject(err));
stream.once('finish', () => resolve());
});
const socketPromise = when.promise((resolve, reject) => {
this.connector.socket.on('data', data => stream.write(data, this.transferType));
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
resolve();
});
this.connector.socket.once('error', err => reject(err));
});
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => when.join(streamPromise, socketPromise))
.finally(() => stream.destroy ? stream.destroy() : null);
})
.then(() => this.reply(226, fileName))
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');

View File

@@ -1,20 +1,19 @@
const _ = require('lodash');
const ENCODING_TYPES = {
A: 'utf8',
I: 'binary',
L: 'binary'
};
module.exports = {
directive: 'TYPE',
handler: function ({command} = {}) {
const encoding = _.upperCase(command.arg);
if (!ENCODING_TYPES.hasOwnProperty(encoding)) return this.reply(501);
this.encoding = ENCODING_TYPES[encoding];
return this.reply(200);
if (/^A[0-9]?$/i.test(command.arg)) {
this.transferType = 'ascii';
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
this.transferType = 'binary';
} else {
return this.reply(501);
}
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
},
syntax: '{{cmd}} <mode>',
description: 'Set the transfer mode, binary (I) or utf8 (A)'
description: 'Set the transfer mode, binary (I) or ascii (A)',
flags: {
feat: 'TYPE A,I,L'
}
};

View File

@@ -21,6 +21,7 @@ const commands = [
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/rest'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),
@@ -35,7 +36,9 @@ const commands = [
require('./registration/type'),
require('./registration/user'),
require('./registration/pbsz'),
require('./registration/prot')
require('./registration/prot'),
require('./registration/eprt'),
require('./registration/epsv')
];
const registry = commands.reduce((result, cmd) => {

View File

@@ -15,8 +15,11 @@ class FtpConnection {
this.id = uuid.v4();
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
this.encoding = 'utf8';
this.bufferSize = false;
this._restByteCount = 0;
this._secure = false;
this.connector = new BaseConnector(this);
@@ -34,7 +37,7 @@ class FtpConnection {
}
_handleData(data) {
const messages = _.compact(data.toString('utf8').split('\r\n'));
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return sequence(messages.map(message => this.commands.handle.bind(this.commands, message)));
}
@@ -47,6 +50,20 @@ class FtpConnection {
}
}
get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined;
}
set restByteCount(rbc) {
this._restByteCount = rbc;
}
get secure() {
return this.server.isTLS || this._secure;
}
set secure(sec) {
this._secure = sec;
}
close(code = 421, message = 'Closing connection') {
return when
.resolve(code)
@@ -58,7 +75,7 @@ class FtpConnection {
return when.try(() => {
const loginListeners = this.server.listeners('login');
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anoymous) throw new errors.GeneralError('No "login" listener setup', 500);
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
} else {
return this.server.emitPromise('login', {connection: this, username, password});
}
@@ -102,7 +119,7 @@ class FtpConnection {
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, packet}, 'Reply');
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, packet}, 'Reply');
letter.socket.write(packet + '\r\n', letter.encoding, err => {
if (err) {
this.log.error(err);

View File

@@ -26,9 +26,9 @@ class Active extends Connector {
return closeExistingServer()
.then(() => {
this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.encoding);
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({ host, port, family }, () => {
this.dataSocket.connect({host, port, family}, () => {
this.dataSocket.pause();
if (this.connection.secure) {

View File

@@ -14,6 +14,10 @@ class Connector {
return this.connection.log;
}
get socket() {
return this.dataSocket;
}
get server() {
return this.connection.server;
}

View File

@@ -41,7 +41,7 @@ class Passive extends Connector {
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.debug({port}, 'Passive connection fulfilled.');
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
@@ -54,20 +54,20 @@ class Passive extends Connector {
this.dataSocket = socket;
}
this.dataSocket.connected = true;
this.dataSocket.setEncoding(this.connection.encoding);
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.debug('Passive connection closed');
this.log.trace('Passive connection closed');
this.end();
});
};
this.dataSocket = null;
this.dataServer = net.createServer({ pauseOnConnect: true }, connectionHandler);
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.log.debug('Passive server closed');
this.log.trace('Passive server closed');
this.dataServer = null;
});
@@ -75,7 +75,7 @@ class Passive extends Connector {
this.dataServer.listen(port, err => {
if (err) reject(err);
else {
this.log.info({port}, 'Passive connection listening');
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
@@ -89,7 +89,8 @@ class Passive extends Connector {
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
} else return undefined;
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}

View File

@@ -8,7 +8,7 @@ const fs = whenNode.liftAll(syncFs);
const errors = require('./errors');
class FileSystem {
constructor(connection, { root, cwd } = {}) {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = cwd || nodePath.sep;
this.root = root || process.cwd();
@@ -65,21 +65,22 @@ class FileSystem {
});
}
write(fileName, {append = false} = {}) {
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+'});
stream.on('error', () => fs.unlink(fsPath));
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlink(fsPath));
stream.once('close', () => stream.end());
return stream;
}
read(fileName) {
read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
return fs.stat(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = syncFs.createReadStream(fsPath, {flags: 'r'});
const stream = syncFs.createReadStream(fsPath, {flags: 'r', start});
return stream;
});
}

View File

@@ -43,21 +43,17 @@ class FtpServer {
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
};
const serverOptions = _.assign(this.isTLS ? this._tls : {}, { pauseOnConnect: true });
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.error(err, '[Event] error'));
if (this.isTLS) {
this.server.on('tlsClientError', err => this.log.error(err, '[Event] tlsClientError'));
}
this.on = this.server.on.bind(this.server);
this.once = this.server.once.bind(this.server);
this.listeners = this.server.listeners.bind(this.server);
process.on('SIGTERM', () => this.close());
process.on('SIGINT', () => this.close());
process.on('SIGBREAK', () => this.close());
process.on('SIGHUP', () => this.close());
process.on('SIGTERM', () => this.quit());
process.on('SIGINT', () => this.quit());
process.on('SIGQUIT', () => this.quit());
}
get isTLS() {
@@ -76,7 +72,7 @@ class FtpServer {
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve();
resolve('Listening');
});
});
});
@@ -94,8 +90,8 @@ class FtpServer {
}
setupTLS(_tls) {
if (!tls) return false;
return _.assign(_tls, {
if (!_tls) return false;
return _.assign({}, _tls, {
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
@@ -129,19 +125,24 @@ class FtpServer {
} catch (err) {
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve();
resolve('Disconnected');
}
});
}
quit() {
return this.close()
.finally(() => process.exit(0));
}
close() {
this.log.info('Server closing...');
this.server.maxConnections = 0;
return when.map(Object.keys(this.connections), id => this.disconnectClient(id))
return when.map(Object.keys(this.connections), id => when.try(this.disconnectClient.bind(this), id))
.then(() => when.promise(resolve => {
this.server.close(err => {
if (err) this.log.error(err, 'Error closing server');
resolve();
resolve('Closed');
});
}));
}

View File

@@ -53,6 +53,29 @@ describe('FtpCommands', function () {
expect(cmd.arg).to.equal('arg1 arg2');
expect(cmd.raw).to.equal('test arg1 arg2');
});
it('two args with quotes: test "hello world"', () => {
const cmd = commands.parse('test "hello world"');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('hello world');
expect(cmd.raw).to.equal('test "hello world"');
});
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg1 arg2');
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
});
it('one arg, with flags: list -l', () => {
const cmd = commands.parse('list -l');
expect(cmd.directive).to.equal('LIST');
expect(cmd.arg).to.equal(null);
expect(cmd.flags).to.deep.equal(['-l']);
expect(cmd.raw).to.equal('list -l');
});
});
describe('handle', function () {

View File

@@ -25,29 +25,25 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful | no active connection', done => {
it('// successful | no active connection', () => {
mockClient.connector.waitForConnection.restore();
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
cmdFn()
return 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()
it('// successful | active connection', () => {
return 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

@@ -19,12 +19,10 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
done();
})
.catch(done);
});
});
});

View File

@@ -22,31 +22,25 @@ describe(CMD, function () {
sandbox.restore();
});
it('TLS // supported', done => {
cmdFn({command: { arg: 'TLS', directive: CMD}})
it('TLS // supported', () => {
return cmdFn({command: {arg: 'TLS', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(234);
expect(mockClient.secure).to.equal(true);
done();
})
.catch(done);
});
});
it('SSL // not supported', done => {
cmdFn({command: { arg: 'SSL', directive: CMD}})
it('SSL // not supported', () => {
return cmdFn({command: {arg: 'SSL', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
});
it('bad // bad', done => {
cmdFn({command: { arg: 'bad', directive: CMD}})
it('bad // bad', () => {
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
});
});

View File

@@ -25,13 +25,11 @@ describe(CMD, function () {
sandbox.restore();
});
it('.. // successful', done => {
cmdFn({log, command: {directive: CMD}})
it('.. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');
done();
})
.catch(done);
});
});
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { chdir: () => {} }
fs: {chdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -23,63 +23,56 @@ describe(CMD, function () {
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs chdir command', done => {
const badMockClient = { reply: () => {}, fs: {} };
it('fails on no fs chdir command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('test // successful', done => {
cmdFn({log, command: { arg: 'test', directive: CMD}})
it('test // successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
done();
})
.catch(done);
});
});
it('test // successful', done => {
it('test // successful', () => {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
cmdFn({log, command: { arg: 'test', directive: CMD}})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
cmdFn({log, command: { arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');
done();
})
.catch(done);
});
});
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { delete: () => {} }
fs: {delete: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -23,51 +23,45 @@ describe(CMD, function () {
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs delete command', done => {
const badMockClient = { reply: () => {}, fs: {} };
it('fails on no fs delete command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('test // successful', done => {
cmdFn({log, command: { arg: 'test', directive: CMD}})
it('test // successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.fs.delete.restore();
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
cmdFn({log, command: { arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');
done();
})
.catch(done);
});
});
});

View File

@@ -0,0 +1,60 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const ActiveConnector = require('../../../src/connector/active');
const CMD = 'EPRT';
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', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('// unsuccessful | invalid argument', () => {
return cmdFn({command: {arg: 'blah'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('// successful IPv4', () => {
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
.then(() => {
const [ip, port, family] = 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');
expect(family).to.equal(4);
});
});
it('// successful IPv6', () => {
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23');
expect(port).to.equal('35286');
expect(family).to.equal(6);
});
});
});

View File

@@ -0,0 +1,35 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const PassiveConnector = require('../../../src/connector/passive');
const CMD = 'EPSV';
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.stub(mockClient, 'reply').resolves();
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
address: () => ({port: 12345})
});
});
afterEach(() => {
sandbox.restore();
});
it('// successful IPv4', () => {
return cmdFn()
.then(() => {
const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229);
expect(message).to.equal('EPSV OK (|||12345|)');
});
});
});

View File

@@ -19,39 +19,31 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn({command: { directive: CMD }})
it('// successful', () => {
return cmdFn({command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
done();
})
.catch(done);
});
});
it('help // successful', done => {
cmdFn({command: { arg: 'help', directive: CMD}})
it('help // successful', () => {
return cmdFn({command: {arg: 'help', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
done();
})
.catch(done);
});
});
it('help // successful', done => {
cmdFn({command: { arg: 'allo', directive: CMD}})
it('allo // successful', () => {
return cmdFn({command: {arg: 'allo', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
cmdFn({command: { arg: 'bad', directive: CMD}})
it('bad // unsuccessful', () => {
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
done();
})
.catch(done);
});
});
});

View File

@@ -87,33 +87,31 @@ describe(CMD, function () {
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs list command', done => {
const badMockClient = { reply: () => {}, fs: {} };
it('fails on no fs list command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('. // successful', done => {
cmdFn({log, command: {directive: CMD}})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(3);
@@ -121,12 +119,10 @@ describe(CMD, function () {
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
done();
})
.catch(done);
});
});
it('testfile.txt // successful', done => {
it('testfile.txt // successful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testfile.txt',
@@ -147,7 +143,7 @@ describe(CMD, function () {
isDirectory: () => false
});
cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(2);
@@ -155,31 +151,25 @@ describe(CMD, function () {
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
done();
})
.catch(done);
});
});
it('. // unsuccessful', done => {
it('. // unsuccessful', () => {
mockClient.fs.list.restore();
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
cmdFn({log, command: {directive: CMD}})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(451);
done();
})
.catch(done);
});
});
it('. // unsuccessful (timeout)', done => {
it('. // unsuccessful (timeout)', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').returns(when.reject(new when.TimeoutError()));
cmdFn({log, command: {directive: CMD}})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();
})
.catch(done);
});
});
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { get: () => {} }
fs: {get: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -23,50 +23,44 @@ describe(CMD, function () {
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs get command', done => {
const badMockClient = { reply: () => {}, fs: {} };
it('fails on no fs get command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('. // successful', done => {
cmdFn({log, command: {directive: CMD}})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
done();
})
.catch(done);
});
});
it('. // unsuccessful', done => {
it('. // unsuccessful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
cmdFn({log, command: {directive: CMD}})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { mkdir: () => {} }
fs: {mkdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -23,63 +23,56 @@ describe(CMD, function () {
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs mkdir command', done => {
const badMockClient = { reply: () => {}, fs: {} };
it('fails on no fs mkdir command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('test // successful', done => {
cmdFn({log, command: {arg: 'test', directive: CMD}})
it('test // successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
done();
})
.catch(done);
});
});
it('test // successful', done => {
it('test // successful', () => {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').resolves('test');
cmdFn({log, command: {arg: 'test', directive: CMD}})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'));
cmdFn({log, command: {arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad');
done();
})
.catch(done);
});
});
});

View File

@@ -19,21 +19,17 @@ describe(CMD, function () {
sandbox.restore();
});
it('S // successful', done => {
cmdFn({command: {arg: 'S'}})
it('S // successful', () => {
return cmdFn({command: {arg: 'S'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
});
it('Q // unsuccessful', done => {
cmdFn({command: {arg: 'Q'}})
it('Q // unsuccessful', () => {
return cmdFn({command: {arg: 'Q'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
});
});

View File

@@ -86,8 +86,8 @@ describe(CMD, function () {
sandbox.restore();
});
it('. // successful', done => {
cmdFn({log, command: {directive: CMD}})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(3);
@@ -95,12 +95,10 @@ describe(CMD, function () {
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
done();
})
.catch(done);
});
});
it('testfile.txt // successful', done => {
it('testfile.txt // successful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testfile.txt',
@@ -121,7 +119,7 @@ describe(CMD, function () {
isDirectory: () => false
});
cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(2);
@@ -129,8 +127,6 @@ describe(CMD, function () {
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
done();
})
.catch(done);
});
});
});

View File

@@ -19,12 +19,10 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
});
});

View File

@@ -19,12 +19,40 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
done();
})
.catch(done);
});
});
it('BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
});
});
it('UTF8 BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'UTF8 BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('UTF8 OFF // successful', () => {
return cmdFn({command: {arg: 'UTF8 OFF', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('ascii');
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
it('UTF8 ON // successful', () => {
return cmdFn({command: {arg: 'UTF8 ON', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('utf8');
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
});

View File

@@ -9,7 +9,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => {},
login: () => {},
server: { options: { anonymous: false } },
server: {options: {anonymous: false}},
username: 'anonymous'
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -24,61 +24,51 @@ describe(CMD, function () {
sandbox.restore();
});
it('pass // successful', done => {
cmdFn({log, command: {arg: 'pass', directive: CMD}})
it('pass // successful', () => {
return cmdFn({log, command: {arg: 'pass', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.args[0]).to.eql(['anonymous', 'pass']);
done();
})
.catch(done);
});
});
it('// successful (already authenticated)', done => {
it('// successful (already authenticated)', () => {
mockClient.server.options.anonymous = true;
mockClient.authenticated = true;
cmdFn({log, command: {directive: CMD}})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
expect(mockClient.login.callCount).to.equal(0);
mockClient.server.options.anonymous = false;
mockClient.authenticated = false;
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects('bad');
cmdFn({log, command: {arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects({});
cmdFn({log, command: {arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
delete mockClient.username;
cmdFn({log, command: {arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
done();
})
.catch(done);
});
});
});

View File

@@ -20,38 +20,32 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful', done => {
cmdFn()
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
done();
})
.catch(done);
});
});
it('// successful', done => {
it('// successful', () => {
mockClient.secure = true;
mockClient.server._tls = {};
cmdFn({command: {arg: '0'}})
return cmdFn({command: {arg: '0'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.bufferSize).to.equal(0);
done();
})
.catch(done);
});
});
it('// successful', done => {
it('// successful', () => {
mockClient.secure = true;
mockClient.server._tls = {};
cmdFn({command: {arg: '10'}})
return cmdFn({command: {arg: '10'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.bufferSize).to.equal(10);
done();
})
.catch(done);
});
});
});

View File

@@ -22,33 +22,27 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful | no argument', done => {
cmdFn()
it('// unsuccessful | no argument', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();
})
.catch(done);
});
});
it('// unsuccessful | invalid argument', done => {
cmdFn({ command: { arg: '1,2,3,4,5' } })
it('// unsuccessful | invalid argument', () => {
return cmdFn({command: {arg: '1,2,3,4,5'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();
})
.catch(done);
});
});
it('// successful', done => {
cmdFn({ command: { arg: '192,168,0,100,137,214' } })
it('// successful', () => {
return cmdFn({command: {arg: '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

@@ -20,56 +20,46 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful', done => {
cmdFn()
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
done();
})
.catch(done);
});
});
it('// unsuccessful - no bufferSize', done => {
it('// unsuccessful - no bufferSize', () => {
mockClient.server._tls = {};
mockClient.secure = true;
cmdFn({command: {arg: 'P'}})
return cmdFn({command: {arg: 'P'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
done();
})
.catch(done);
});
});
it('// successful', done => {
it('// successful', () => {
mockClient.bufferSize = 0;
mockClient.secure = true;
cmdFn({command: {arg: 'p'}})
return cmdFn({command: {arg: 'p'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
});
it('// unsuccessful - unsupported', done => {
it('// unsuccessful - unsupported', () => {
mockClient.secure = true;
cmdFn({command: {arg: 'C'}})
return cmdFn({command: {arg: 'C'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(536);
done();
})
.catch(done);
});
});
it('// unsuccessful - unknown', done => {
it('// unsuccessful - unknown', () => {
mockClient.secure = true;
cmdFn({command: {arg: 'QQ'}})
return cmdFn({command: {arg: 'QQ'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
});
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { currentDirectory: () => {} }
fs: {currentDirectory: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -23,61 +23,53 @@ describe(CMD, function () {
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs currentDirectory command', done => {
const badMockClient = { reply: () => {}, fs: {} };
it('fails on no fs currentDirectory command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
badCmdFn()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('// successful', done => {
cmdFn({log, command: { arg: 'test', directive: CMD}})
it('// successful', () => {
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
done();
})
.catch(done);
});
});
it('// successful', done => {
it('// successful', () => {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test');
cmdFn({log, command: {arg: 'test', directive: CMD}})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
done();
})
.catch(done);
});
});
it('// unsuccessful', done => {
it('// unsuccessful', () => {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'));
cmdFn({log, command: {arg: 'bad', directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
});

View File

@@ -18,12 +18,10 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.close.callCount).to.equal(1);
done();
})
.catch(done);
});
});
});

View File

@@ -0,0 +1,58 @@
const {expect} = require('chai');
const sinon = require('sinon');
const when = require('when');
const CMD = 'REST';
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('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('-1 // unsuccessful', () => {
return cmdFn({command: {arg: '-1', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('bad // unsuccessful', () => {
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('1 // successful', () => {
return cmdFn({command: {arg: '1', directive: CMD}})
.then(() => {
expect(mockClient.restByteCount).to.equal(1);
expect(mockClient.reply.args[0][0]).to.equal(350);
});
});
it('0 // successful', () => {
return cmdFn({command: {arg: '0', directive: CMD}})
.then(() => {
expect(mockClient.restByteCount).to.equal(0);
expect(mockClient.reply.args[0][0]).to.equal(350);
});
});
});

View File

@@ -0,0 +1,75 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'RETR';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
commandSocket: {
pause: () => {},
resume: () => {}
},
reply: () => when.resolve(),
connector: {
waitForConnection: () => when.resolve({
resume: () => {}
}),
end: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.fs = {
read: () => {}
};
sandbox.spy(mockClient, 'reply');
});
afterEach(() => sandbox.restore());
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
});
it('// unsuccessful | connector times out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new when.TimeoutError());
});
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
it('// unsuccessful | connector errors out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new Error('test'));
});
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(551);
});
});
});

View File

@@ -5,8 +5,8 @@ const sinon = require('sinon');
const CMD = 'RNFR';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
@@ -24,47 +24,39 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('// unsuccessful | file system does not have functions', done => {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
it('test // unsuccessful | file get fails', done => {
it('test // unsuccessful | file get fails', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { arg: 'test' } })
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('test // successful', done => {
cmdFn({ log: mockLog, command: { arg: 'test' } })
it('test // successful', () => {
return cmdFn({log: mockLog, command: {arg: '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

@@ -5,8 +5,8 @@ const sinon = require('sinon');
const CMD = 'RNTO';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
@@ -25,58 +25,48 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful | no renameFrom set', done => {
it('// unsuccessful | no renameFrom set', () => {
delete mockClient.renameFrom;
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
done();
})
.catch(done);
});
});
it('// unsuccessful | no file system', done => {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('// unsuccessful | file system does not have functions', done => {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
it('new // unsuccessful | rename fails', done => {
it('new // unsuccessful | rename fails', () => {
mockClient.fs.rename.restore();
sandbox.stub(mockClient.fs, 'rename').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { arg: 'new' } })
return cmdFn({log: mockLog, command: {arg: 'new'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('new // successful', done => {
cmdFn({ command: { arg: 'new' } })
it('new // successful', () => {
return cmdFn({command: {arg: '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

@@ -5,8 +5,8 @@ const sinon = require('sinon');
const CMD = 'CHMOD';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()};
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
beforeEach(() => {
@@ -49,7 +49,7 @@ describe(CMD, function () {
mockClient.fs.chmod.restore();
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { arg: '777 test' } })
cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
done();
@@ -58,7 +58,7 @@ describe(CMD, function () {
});
it('777 test // successful', done => {
cmdFn({ log: mockLog, command: { arg: '777 test' } })
cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => {
expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]);
expect(mockClient.reply.args[0][0]).to.equal(200);

View File

@@ -0,0 +1,52 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan');
const siteRegistry = require('../../../../src/commands/registration/site/registry');
const FtpCommands = require('../../../../src/commands');
const CMD = 'SITE';
describe(CMD, function () {
let sandbox;
const log = bunyan.createLogger({name: 'site-test'});
const mockClient = {
reply: () => when.resolve(),
commands: new FtpCommands()
};
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves();
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful', () => {
return cmdFn({log})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});
});
it('// unsuccessful', () => {
return cmdFn({log, command: {arg: 'BAD'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});
});
it('// successful', () => {
sandbox.stub(siteRegistry.CHMOD, 'handler').resolves();
return cmdFn({log, command: {arg: 'CHMOD test'}})
.then(() => {
const {command} = siteRegistry.CHMOD.handler.args[0][0];
expect(command.directive).to.equal('CHMOD');
expect(command.arg).to.equal('test');
});
});
});

View File

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

View File

@@ -5,8 +5,8 @@ const sinon = require('sinon');
const CMD = 'STAT';
describe(CMD, function () {
let sandbox;
const mockLog = { error: () => {} };
const mockClient = { reply: () => when.resolve() };
const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
@@ -23,49 +23,41 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
done();
})
.catch(done);
});
});
it('// unsuccessful | no file system', done => {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
cmdFn({ command: { arg: 'test' } })
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('// unsuccessful | file system does not have functions', done => {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
cmdFn({ command: { arg: 'test' } })
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
it('// unsuccessful | file get fails', done => {
it('// unsuccessful | file get fails', () => {
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
cmdFn({ log: mockLog, command: { arg: 'test' } })
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(450);
done();
})
.catch(done);
});
});
it('// successful | file', done => {
it('// successful | file', () => {
sandbox.stub(mockClient.fs, 'get').returns({
name: 'test_file',
dev: 2114,
@@ -85,15 +77,13 @@ describe(CMD, function () {
isDirectory: () => false
});
cmdFn({ command: { arg: 'test' } })
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(212);
done();
})
.catch(done);
});
});
it('// successful | directory', done => {
it('// successful | directory', () => {
sandbox.stub(mockClient.fs, 'list').returns([{
name: 'test_file',
dev: 2114,
@@ -132,11 +122,9 @@ describe(CMD, function () {
isDirectory: () => true
});
cmdFn({ command: { arg: 'test' } })
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
done();
})
.catch(done);
});
});
});

View File

@@ -0,0 +1,75 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'STOR';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
commandSocket: {
pause: () => {},
resume: () => {}
},
reply: () => when.resolve(),
connector: {
waitForConnection: () => when.resolve({
resume: () => {}
}),
end: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.fs = {
write: () => {}
};
sandbox.spy(mockClient, 'reply');
});
afterEach(() => sandbox.restore());
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
});
it('// unsuccessful | connector times out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new when.TimeoutError());
});
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
it('// unsuccessful | connector errors out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new Error('test'));
});
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
});

View File

@@ -30,54 +30,46 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('// unsuccessful | file system does not have functions', done => {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
cmdFn()
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
it('// successful | given name is unique', done => {
it('// successful | given name is unique', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects({});
cmdFn({ command: { arg: 'good' } })
return cmdFn({command: {arg: 'good'}})
.then(() => {
const call = stor.handler.call.args[0][1];
expect(call).to.have.property('command');
expect(call.command).to.have.property('arg');
expect(call.command.arg).to.eql('good');
expect(mockClient.fs.getUniqueName.callCount).to.equal(0);
done();
})
.catch(done);
});
});
it('// successful | generates unique name', done => {
cmdFn({ command: { arg: 'bad' } })
it('// successful | generates unique name', () => {
return cmdFn({command: {arg: 'bad'}})
.then(() => {
const call = stor.handler.call.args[0][1];
expect(call).to.have.property('command');
expect(call.command).to.have.property('arg');
expect(call.command.arg).to.eql('4');
expect(mockClient.fs.getUniqueName.callCount).to.equal(1);
done();
})
.catch(done);
});
});
});

View File

@@ -19,21 +19,17 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn({command: { arg: 'F' } })
it('// successful', () => {
return cmdFn({command: {arg: 'F'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
done();
})
.catch(done);
});
});
it('// unsuccessful', done => {
cmdFn({command: { arg: 'X' } })
it('// unsuccessful', () => {
return cmdFn({command: {arg: 'X'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
done();
})
.catch(done);
});
});
});

View File

@@ -19,12 +19,10 @@ describe(CMD, function () {
sandbox.restore();
});
it('// successful', done => {
cmdFn()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(215);
done();
})
.catch(done);
});
});
});

View File

@@ -13,50 +13,42 @@ describe(CMD, function () {
beforeEach(() => {
sandbox = sinon.sandbox.create();
mockClient.encoding = null;
mockClient.transferType = null;
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('A // successful', done => {
cmdFn({ command: { arg: 'A' } })
it('A // successful', () => {
return cmdFn({command: {arg: 'A'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.encoding).to.equal('utf8');
done();
})
.catch(done);
expect(mockClient.transferType).to.equal('ascii');
});
});
it('I // successful', done => {
cmdFn({ command: { arg: 'I' } })
it('I // successful', () => {
return cmdFn({command: {arg: 'I'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.encoding).to.equal('binary');
done();
})
.catch(done);
expect(mockClient.transferType).to.equal('binary');
});
});
it('L // successful', done => {
cmdFn({ command: { arg: 'L' } })
it('L // successful', () => {
return cmdFn({command: {arg: 'L'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.encoding).to.equal('binary');
done();
})
.catch(done);
expect(mockClient.transferType).to.equal('binary');
});
});
it('X // successful', done => {
cmdFn({ command: { arg: 'X' } })
it('X // successful', () => {
return cmdFn({command: {arg: 'X'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
expect(mockClient.encoding).to.equal(null);
done();
})
.catch(done);
expect(mockClient.transferType).to.equal(null);
});
});
});

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
};
const mockClient = {
reply: () => when.resolve(),
server: { options: {} },
server: {options: {}},
login: () => when.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
@@ -28,70 +28,80 @@ describe(CMD, function () {
sandbox.restore();
});
it('test // successful | prompt for password', done => {
cmdFn({ command: { arg: 'test' } })
it('test // successful | prompt for password', () => {
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(331);
done();
})
.catch(done);
});
});
it('test // successful | anonymous login', done => {
it('test // successful | anonymous login', () => {
mockClient.server.options = {anonymous: true};
cmdFn({ command: { arg: 'anonymous' } })
return cmdFn({command: {arg: 'anonymous'}})
.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: { } })
it('test // unsuccessful | no username provided', () => {
return cmdFn({command: { }})
.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 => {
it('test // unsuccessful | already set username', () => {
mockClient.username = 'test';
cmdFn({ command: { arg: 'test' } })
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
expect(mockClient.login.callCount).to.equal(0);
done();
})
.catch(done);
});
});
it('test // successful | regular login if anonymous is true', done => {
it('test // successful | regular login if anonymous is true', () => {
mockClient.server.options = {anonymous: true};
cmdFn({ log: mockLog, command: { arg: 'test' } })
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(331);
expect(mockClient.login.callCount).to.equal(0);
done();
})
.catch(done);
});
});
it('test // successful | anonymous login with set username', done => {
it('test // successful | anonymous login with set username', () => {
mockClient.server.options = {anonymous: 'sillyrabbit'};
cmdFn({ log: mockLog, command: { arg: 'sillyrabbit' } })
return cmdFn({log: mockLog, command: {arg: 'sillyrabbit'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(1);
done();
})
.catch(done);
});
});
it('test // unsuccessful | anonymous login fails', () => {
mockClient.server.options = {anonymous: true};
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects(new Error('test'));
return cmdFn({log: mockLog, command: {arg: 'anonymous'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
expect(mockClient.login.callCount).to.equal(1);
});
});
it('test // successful | does not login if already authenticated', () => {
mockClient.authenticated = true;
return cmdFn({log: mockLog, command: {arg: 'sillyrabbit'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(0);
});
});
});

View File

@@ -3,6 +3,7 @@ const {expect} = require('chai');
const sinon = require('sinon');
const net = require('net');
const tls = require('tls');
const ActiveConnector = require('../../src/connector/active');
const findPort = require('../../src/helpers/find-port');
@@ -33,34 +34,49 @@ describe('Connector - Active //', function () {
server.close(done);
});
it('sets up a connection', function (done) {
active.setupConnection('127.0.0.1', PORT)
it('sets up a connection', function () {
return 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) {
it('destroys existing connection, then sets up a connection', function () {
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
active.setupConnection('127.0.0.1', PORT)
return 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)
it('waits for connection', function () {
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then(() => done())
.catch(done);
.then(dataSocket => {
expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(false);
});
});
it('upgrades to a secure connection', function () {
mockConnection.secure = true;
mockConnection.server = {_tls: {}};
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then(dataSocket => {
expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(true);
});
});
});

View File

@@ -16,10 +16,14 @@ describe('Connector - Passive //', function () {
encoding: 'utf8',
log: bunyan.createLogger({name: 'passive-test'}),
commandSocket: {},
server: { options: {} }
server: {options: {}}
};
let sandbox;
function shouldNotResolve() {
throw new Error('Should not resolve');
}
before(() => {
passive = new PassiveConnector(mockConnection);
});
@@ -36,45 +40,49 @@ describe('Connector - Passive //', function () {
sandbox.restore();
});
it('cannot wait for connection with no server', function (done) {
passive.waitForConnection()
.then(() => done('should not happen'))
it('cannot wait for connection with no server', function () {
return passive.waitForConnection()
.then(shouldNotResolve)
.catch(err => {
expect(err.name).to.equal('ConnectorError');
done();
});
});
it('has invalid pasv range', function (done) {
it('no pasv range provided', function () {
delete mockConnection.server.options.pasv_range;
return passive.setupServer()
.then(shouldNotResolve)
.catch(err => {
expect(err.name).to.equal('ConnectorError');
});
});
it('has invalid pasv range', function () {
mockConnection.server.options.pasv_range = -1;
passive.setupServer()
.then(() => done('should not happen'))
return passive.setupServer()
.then(shouldNotResolve)
.catch(err => {
expect(err.name).to.equal('RangeError');
done();
});
});
it('sets up a server', function (done) {
passive.setupServer()
it('sets up a server', function () {
return passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
done();
})
.catch(done);
});
});
it('destroys existing server, then sets up a server', function (done) {
it('destroys existing server, then sets up a server', function () {
const closeFnSpy = sandbox.spy(passive.dataServer, 'close');
passive.setupServer()
return 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) {
@@ -97,8 +105,8 @@ describe('Connector - Passive //', function () {
.catch(done);
});
it('accepts connection', function (done) {
passive.setupServer()
it('accepts connection', function () {
return passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
@@ -109,8 +117,6 @@ describe('Connector - Passive //', function () {
.then(() => {
expect(passive.dataSocket).to.exist;
passive.end();
done();
})
.catch(done);
});
});
});

View File

@@ -2,10 +2,9 @@ const {expect} = require('chai');
const escapePath = require('../../src/helpers/escape-path');
describe('helpers // escape-path', function () {
it('escapes quotes', done => {
it('escapes quotes', () => {
const string = '"test"';
const escapedString = escapePath(string);
expect(escapedString).to.equal('""test""');
done();
});
});

View File

@@ -17,22 +17,19 @@ describe('helpers // find-port', function () {
sandbox.restore();
});
it('finds a port', done => {
findPort(1)
it('finds a port', () => {
return findPort(1)
.then(port => {
expect(Server.prototype.listen.callCount).to.be.above(1);
expect(port).to.be.above(1);
done();
})
.catch(done);
});
});
it('does not find a port', done => {
findPort(1, 2)
.then(() => done('no'))
it('does not find a port', () => {
return findPort(1, 2)
.then(() => expect(1).to.equal(2)) // should not happen
.catch(err => {
expect(err).to.exist;
done();
});
});
});

View File

@@ -1,36 +1,51 @@
const {expect} = require('chai');
const sinon = require('sinon');
const resolveHost = require('../../src/helpers/resolve-host');
describe('helpers //resolve-host', function () {
this.timeout(4000);
it('fetches ip address', done => {
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
afterEach(() => sandbox.restore());
it('fetches ip address', () => {
const hostname = '0.0.0.0';
resolveHost(hostname)
return resolveHost(hostname)
.then(resolvedHostname => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
done();
})
.catch(done);
});
});
it('fetches ip address', done => {
it('fetches ip address', () => {
const hostname = null;
resolveHost(hostname)
return resolveHost(hostname)
.then(resolvedHostname => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
done();
})
.catch(done);
});
});
it('does nothing', done => {
it('does nothing', () => {
const hostname = '127.0.0.1';
resolveHost(hostname)
return resolveHost(hostname)
.then(resolvedHostname => {
expect(resolvedHostname).to.equal(hostname);
done();
})
.catch(done);
});
});
it('fails on getting hostname', () => {
sandbox.stub(require('http'), 'get').callsFake(function (url, cb) {
cb({
statusCode: 420
});
});
return resolveHost(null)
.then(() => expect(1).to.equal(2))
.catch(err => {
expect(err.code).to.equal(420);
});
});
});

View File

@@ -1,5 +1,6 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan');
const fs = require('fs');
@@ -10,11 +11,14 @@ before(() => require('dotenv').load());
describe('FtpServer', function () {
this.timeout(2000);
let sandbox;
let log = bunyan.createLogger({name: 'test'});
let server;
let client;
before(done => {
let connection;
before(() => {
server = new FtpServer(process.env.FTP_URL, {
log,
pasv_range: process.env.PASV_RANGE,
@@ -22,14 +26,21 @@ describe('FtpServer', function () {
key: `${process.cwd()}/test/cert/server.key`,
cert: `${process.cwd()}/test/cert/server.crt`,
ca: `${process.cwd()}/test/cert/server.csr`
}
},
greeting: ['hello', 'world']
});
server.on('login', (data, resolve) => {
connection = data.connection;
resolve({root: process.cwd()});
});
server.listen()
.then(() => done());
return server.listen();
});
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
afterEach(() => {
sandbox.restore();
});
after(() => {
server.close();
@@ -109,12 +120,28 @@ describe('FtpServer', function () {
});
});
it('STOR test.txt', done => {
it('STOR fail.txt', done => {
sandbox.stub(connection.fs, 'write').callsFake(function () {
const fsPath = './test/fail.txt';
const stream = require('fs').createWriteStream(fsPath, {flags: 'w+'});
stream.once('error', () => fs.unlink(fsPath));
setTimeout(() => stream.emit('error', new Error('STOR fail test'), 1));
return stream;
});
const buffer = Buffer.from('test text file');
client.put(buffer, 'test.txt', err => {
client.put(buffer, 'fail.txt', err => {
expect(err).to.exist;
expect(fs.existsSync('./test/fail.txt')).to.equal(false);
done();
});
});
it('STOR tést.txt', done => {
const buffer = Buffer.from('test text file');
client.put(buffer, 'tést.txt', err => {
expect(err).to.not.exist;
expect(fs.existsSync('./test/test.txt')).to.equal(true);
fs.readFile('./test/test.txt', (fserr, data) => {
expect(fs.existsSync('./test/tést.txt')).to.equal(true);
fs.readFile('./test/tést.txt', (fserr, data) => {
expect(fserr).to.not.exist;
expect(data.toString()).to.equal('test text file');
done();
@@ -122,11 +149,11 @@ describe('FtpServer', function () {
});
});
it('APPE test.txt', done => {
it('APPE tést.txt', done => {
const buffer = Buffer.from(', awesome!');
client.append(buffer, 'test.txt', err => {
client.append(buffer, 'tést.txt', err => {
expect(err).to.not.exist;
fs.readFile('./test/test.txt', (fserr, data) => {
fs.readFile('./test/tést.txt', (fserr, data) => {
expect(fserr).to.not.exist;
expect(data.toString()).to.equal('test text file, awesome!');
done();
@@ -134,8 +161,8 @@ describe('FtpServer', function () {
});
});
it('RETR test.txt', done => {
client.get('test.txt', (err, stream) => {
it('RETR tést.txt', done => {
client.get('tést.txt', (err, stream) => {
expect(err).to.not.exist;
let text = '';
stream.on('data', data => {
@@ -148,10 +175,10 @@ describe('FtpServer', function () {
});
});
it('RNFR test.txt, RNTO awesome.txt', done => {
client.rename('test.txt', 'awesome.txt', err => {
it('RNFR tést.txt, RNTO awesome.txt', done => {
client.rename('tést.txt', 'awesome.txt', err => {
expect(err).to.not.exist;
expect(fs.existsSync('./test/test.txt')).to.equal(false);
expect(fs.existsSync('./test/tést.txt')).to.equal(false);
expect(fs.existsSync('./test/awesome.txt')).to.equal(true);
fs.readFile('./test/awesome.txt', (fserr, data) => {
expect(fserr).to.not.exist;
@@ -196,19 +223,19 @@ describe('FtpServer', function () {
});
});
it('MKD tmp', done => {
if (fs.existsSync('./test/tmp')) {
fs.rmdirSync('./test/tmp');
it('MKD témp', done => {
if (fs.existsSync('./test/témp')) {
fs.rmdirSync('./test/témp');
}
client.mkdir('tmp', err => {
client.mkdir('témp', err => {
expect(err).to.not.exist;
expect(fs.existsSync('./test/tmp')).to.equal(true);
expect(fs.existsSync('./test/témp')).to.equal(true);
done();
});
});
it('CWD tmp', done => {
client.cwd('tmp', (err, data) => {
it('CWD témp', done => {
client.cwd('témp', (err, data) => {
expect(err).to.not.exist;
expect(data).to.be.a('string');
done();
@@ -222,10 +249,10 @@ describe('FtpServer', function () {
});
});
it('RMD tmp', done => {
client.rmdir('tmp', err => {
it('RMD témp', done => {
client.rmdir('témp', err => {
expect(err).to.not.exist;
expect(fs.existsSync('./test/tmp')).to.equal(false);
expect(fs.existsSync('./test/témp')).to.equal(false);
done();
});
});

View File

@@ -1,5 +0,0 @@
{
"mute":false,
"level":"fatal",
"reporter":"spec"
}

View File

@@ -19,7 +19,7 @@ const server = new FtpServer('ftp://127.0.0.1:8880', {
});
server.on('login', ({username, password}, resolve, reject) => {
if (username === 'test' && password === 'test' || username === 'anonymous') {
resolve({ root: require('os').homedir() });
resolve({root: require('os').homedir()});
} else reject('Bad username or password');
});
server.listen();