diff --git a/CHANGELOG.md b/CHANGELOG.md index 719e5a6..6b72f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +### v0.0.15 +* feat(fs): support relative paths +* fix(util.loadFile): do not override the exceptions + +### v0.0.14 +* http.ServerResponse: publish headerSent, getHeader, write +* http.ServerRequest: add getHeader + +### v0.0.13 +* Add http.ServerRequest.headers + +### v0.0.12 +* http.ServerResponse emit "end" event + ### v0.0.11 * Added exists/existsSync to fs mocks diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..58d61e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License + +Copyright (C) 2012-2013 Vojta Jína. + +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 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. diff --git a/lib/fs.js b/lib/fs.js index 887dcbd..a3ceece 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1,4 +1,3 @@ -// TODO(vojta): allow relative paths var util = require('util'); var path = require('path'); var predictableNextTick = require('./util.js').predictableNextTick; @@ -50,11 +49,13 @@ var Directory = function(mtime) { */ var Mock = function(structure) { var watchers = {}; + var cwd = '/'; // TODO(vojta): convert structure to contain only instances of File/Directory (not primitives) - var getPointer = function(path, pointer) { - var parts = path.replace(/\\/g, '/').replace(/\/$/, '').split('/').slice(1); + var getPointer = function(filepath, pointer) { + var absPath = path.resolve(cwd, filepath); + var parts = absPath.replace(/\\/g, '/').replace(/\/$/, '').split('/').slice(1); while (parts.length) { if (!pointer[parts[0]]) break; @@ -275,6 +276,10 @@ var Mock = function(structure) { callback(current, previous); }); }; + + this._setCWD = function(path) { + cwd = path; + }; }; diff --git a/lib/http.js b/lib/http.js index 548ee80..c6ceb4f 100644 --- a/lib/http.js +++ b/lib/http.js @@ -3,49 +3,70 @@ var EventEmitter = require('events').EventEmitter; var ServerResponse = function() { - var headSent = false; var bodySent = false; this._headers = {}; this._body = null; + this._status = null; this._isFinished = function() { - return headSent && bodySent; + return this.headerSent && bodySent; }; + this.headerSent = false; + this.setHeader = function(name, value) { - if (headSent) { + if (this.headerSent) { throw new Error("Can't set headers after they are sent."); } this._headers[name] = value; }; + this.getHeader = function(name) { + return this._headers[name]; + }; + this.removeHeader = function(name) { delete this._headers[name]; }; this.writeHead = function(status) { - if (headSent) { + if (this.headerSent) { throw new Error("Can't render headers after they are sent to the client."); } - headSent = true; + this.headerSent = true; this._status = status; }; - this.end = function(body) { - if (bodySent) return; + this.write = function(content) { + if (bodySent) { + throw new Error("Can't write to already finished response."); + } + + this._body = this._body ? this._body + content.toString() : content.toString(); + }; + + this.end = function(content) { + if (content) { + this.write(content ); + } bodySent = true; - this._body = body.toString(); + this.emit('end'); }; }; util.inherits(ServerResponse, EventEmitter); -var ServerRequest = function(url) { +var ServerRequest = function(url, headers) { this.url = url; + this.headers = headers || {}; + + this.getHeader = function(key) { + return this.headers[key]; + }; }; util.inherits(ServerRequest, EventEmitter); diff --git a/lib/util.js b/lib/util.js index ab24a68..a5d4a9a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,6 +1,7 @@ var vm = require('vm'); var fs = require('fs'); var path = require('path'); +var coffee = require('coffee-script') var nextTickQueue = []; var nextTickRegistered = false; @@ -81,8 +82,9 @@ var loadFile = function(filePath, mocks, globals, mockNested) { globals = globals || {}; filePath = path.normalize(filePath); - - if (filePath.substr(-3) !== '.js') { + var extname = path.extname(filePath); + + if (extname !== '.js' && extname !== '.coffee' && extname !== '.litcoffee'){ filePath += '.js'; } @@ -128,7 +130,12 @@ var loadFile = function(filePath, mocks, globals, mockNested) { }); try { - vm.runInNewContext(fs.readFileSync(filePath), context, filePath); + if (extname === '.coffee' || extname === '.litcoffee') { + jsSource = compileCoffeeScript(filePath); + } else { + jsSource = fs.readFileSync(filePath); + } + vm.runInNewContext(jsSource, context); } catch (e) { e.stack = e.name + ': ' + e.message + '\n' + '\tat ' + filePath; @@ -139,6 +146,20 @@ var loadFile = function(filePath, mocks, globals, mockNested) { return context; }; +/** + * compile coffeescript + * + * @param {string} filePath Absolute path to module (file to load) + * @return {string} javascript source + */ +function compileCoffeeScript(filePath) { + var coffeeSource = fs.readFileSync(filePath, {encoding: 'utf8'}); + var options = {encoding: 'utf8', bare: true, literate: false}; + if (filePath.substr(-10) === '.litcoffee') { + options.literate = true; + } + return coffee.compile(coffeeSource, options); +} // PUBLIC stuff exports.loadFile = loadFile; diff --git a/package.json b/package.json index 31bbd30..a2323a9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "Damien Duportal ", "Taichi " ], - "dependencies": {}, + "dependencies": { + "coffee-script": ">=1.5.0" + }, "devDependencies": { "coffee-script": "1.1.2", "jasmine-node": "1.0.11", @@ -37,5 +39,5 @@ "engines": { "node": ">= 0.6.5" }, - "version": "0.0.11" -} \ No newline at end of file + "version": "0.0.15" +} diff --git a/test/fixtures/other.coffee b/test/fixtures/other.coffee new file mode 100644 index 0000000..3b3183e --- /dev/null +++ b/test/fixtures/other.coffee @@ -0,0 +1,4 @@ +#A fake module for testing util.loadFile() + +exports.id = 'LOCAL_MODULE' +exports.fs = require 'fs' diff --git a/test/fixtures/other.litcoffee b/test/fixtures/other.litcoffee new file mode 100644 index 0000000..3f3f780 --- /dev/null +++ b/test/fixtures/other.litcoffee @@ -0,0 +1,4 @@ +A fake module for testing util.loadFile() + + exports.id = 'LOCAL_MODULE' + exports.fs = require 'fs' diff --git a/test/fixtures/some.coffee b/test/fixtures/some.coffee new file mode 100644 index 0000000..5494816 --- /dev/null +++ b/test/fixtures/some.coffee @@ -0,0 +1,6 @@ +# A fake module for testing util.loadFile() + +privateNumber = 100 +privateFs = require 'fs' +privateLocalModule = require './other.coffee' +privateConsole = console diff --git a/test/fixtures/some.litcoffee b/test/fixtures/some.litcoffee new file mode 100644 index 0000000..dc6558e --- /dev/null +++ b/test/fixtures/some.litcoffee @@ -0,0 +1,7 @@ +A fake module for testing util.loadFile() + + privateNumber = 100 + privateFs = require 'fs' + privateLocalModule = require './other.litcoffee' + privateConsole = console + diff --git a/test/fs.spec.coffee b/test/fs.spec.coffee index e618df3..af52a49 100644 --- a/test/fs.spec.coffee +++ b/test/fs.spec.coffee @@ -387,3 +387,17 @@ describe 'fs', -> fs.exists '/home/vojta/none-existing.js', callback waitForFinished() + + + describe 'relative paths', -> + + it 'should read file with a relative path', -> + callback = (err, data) -> + expect(err).toBeFalsy() + expect(data instanceof Buffer).toBe true + expect(data.toString()).toBe 'some' + finished++ + + fs._setCWD '/home/vojta' + fs.readFile './some.js', callback + waitForFinished() diff --git a/test/http.spec.coffee b/test/http.spec.coffee index 532f6bc..853e1fe 100644 --- a/test/http.spec.coffee +++ b/test/http.spec.coffee @@ -28,9 +28,11 @@ describe 'http', -> expect(response._status).toBe 201 - it 'should ignore end() when already sent', -> + it 'should throw when trygin to end() already finished reponse', -> response.end 'First Body' - response.end 'Another Body' + + expect(-> response.end 'Another Body') + .toThrow "Can't write to already finished response." expect(response._body).toBe 'First Body' @@ -43,6 +45,11 @@ describe 'http', -> expect(response._headers).toEqual {'Content-Type': 'text/plain'} + it 'should getHeader()', -> + response.setHeader 'Content-Type', 'json' + expect(response.getHeader 'Content-Type').toBe 'json' + + it 'should throw when trying to send headers twice', -> response.writeHead 200 @@ -57,6 +64,21 @@ describe 'http', -> .toThrow "Can't set headers after they are sent." + it 'should write body', -> + response.write 'a' + response.end 'b' + + expect(response._body).toBe 'ab' + + + it 'should throw when trying to write after end', -> + response.write 'one' + response.end 'two' + + expect(-> response.write 'more') + .toThrow "Can't write to already finished response." + + it 'isFinished() should assert whether headers and body has been sent', -> expect(response._isFinished()).toBe false @@ -68,3 +90,13 @@ describe 'http', -> response.end 'Some body' expect(response._isFinished()).toBe true + + + #============================================================================== + # http.ServerRequest + #============================================================================== + describe 'ServerRequest', -> + + it 'should return headers', -> + request = new httpMock.ServerRequest '/some', {'Content-Type': 'json'} + expect(request.getHeader 'Content-Type').toBe 'json' diff --git a/test/util.spec.coffee b/test/util.spec.coffee index 501df27..30db922 100644 --- a/test/util.spec.coffee +++ b/test/util.spec.coffee @@ -180,3 +180,81 @@ describe 'mock-util', -> expect(module.privateLocalModule.fs).toBe fsMock + + #============================================================================ + # util.loadFile() CoffeeScript + #============================================================================ + describe 'loadFile', -> + loadFile = util.loadFile + fixturePath = __dirname + '/fixtures/some.coffee' + + it 'should load file with access to private state', -> + module = loadFile fixturePath + expect(module.privateNumber).toBe 100 + + + it 'should inject mocks', -> + fsMock = {} + module = loadFile fixturePath, {fs: fsMock} + expect(module.privateFs).toBe fsMock + + + it 'should load local modules', -> + module = loadFile fixturePath + expect(module.privateLocalModule).toBeDefined() + expect(module.privateLocalModule.id).toBe 'LOCAL_MODULE' + + + it 'should inject globals', -> + fakeConsole = {} + module = loadFile fixturePath, {}, {console: fakeConsole} + expect(module.privateConsole).toBe fakeConsole + + + it 'should inject mocks into nested modules', -> + fsMock = {} + + # /fixtures/some.coffee requires /fixtures/other.coffee + # /fixtures/other.coffee requires fs + module = loadFile fixturePath, {fs: fsMock}, {}, true + + expect(module.privateLocalModule.fs).toBe fsMock + + #============================================================================ + # util.loadFile() literate style CoffeeScript + #============================================================================ + describe 'loadFile', -> + loadFile = util.loadFile + fixturePath = __dirname + '/fixtures/some.litcoffee' + + it 'should load file with access to private state', -> + module = loadFile fixturePath + expect(module.privateNumber).toBe 100 + + + it 'should inject mocks', -> + fsMock = {} + module = loadFile fixturePath, {fs: fsMock} + expect(module.privateFs).toBe fsMock + + + it 'should load local modules', -> + module = loadFile fixturePath + expect(module.privateLocalModule).toBeDefined() + expect(module.privateLocalModule.id).toBe 'LOCAL_MODULE' + + + it 'should inject globals', -> + fakeConsole = {} + module = loadFile fixturePath, {}, {console: fakeConsole} + expect(module.privateConsole).toBe fakeConsole + + + it 'should inject mocks into nested modules', -> + fsMock = {} + + # /fixtures/some.litcoffee requires /fixtures/other.litcoffee + # /fixtures/other.litcoffee requires fs + module = loadFile fixturePath, {fs: fsMock}, {}, true + + expect(module.privateLocalModule.fs).toBe fsMock