', test.title, encodeURIComponent(test.fullTitle()));
var str = test.err.stack || test.err.toString();
// FF / Opera do not add the message
if (!~str.indexOf(test.err.message)) {
str = test.err.message + '\n' + str;
}
// <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
// check for the result of the stringifying.
if ('[object Error]' == str) str = test.err.message;
// Safari doesn't give you a stack. Let's at least provide a source line.
if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) {
str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")";
}
el.appendChild(fragment('
%e
', str));
}
// toggle code
// TODO: defer
if (!test.pending) {
var h2 = el.getElementsByTagName('h2')[0];
on(h2, 'click', function(){
pre.style.display = 'none' == pre.style.display
? 'block'
: 'none';
});
var pre = fragment('
%e
', utils.clean(test.fn.toString()));
el.appendChild(pre);
pre.style.display = 'none';
}
// Don't call .appendChild if #mocha-report was already .shift()'ed off the stack.
if (stack[0]) stack[0].appendChild(el);
});
}
/**
* Display error `msg`.
*/
function error(msg) {
document.body.appendChild(fragment('
%s
', msg));
}
/**
* Return a DOM fragment from `html`.
*/
function fragment(html) {
var args = arguments
, div = document.createElement('div')
, i = 1;
div.innerHTML = html.replace(/%([se])/g, function(_, type){
switch (type) {
case 's': return String(args[i++]);
case 'e': return escape(args[i++]);
}
});
return div.firstChild;
}
/**
* Check for suites that do not have elements
* with `classname`, and hide them.
*/
function hideSuitesWithout(classname) {
var suites = document.getElementsByClassName('suite');
for (var i = 0; i < suites.length; i++) {
var els = suites[i].getElementsByClassName(classname);
if (0 == els.length) suites[i].className += ' hidden';
}
}
/**
* Unhide .hidden suites.
*/
function unhide() {
var els = document.getElementsByClassName('suite hidden');
for (var i = 0; i < els.length; ++i) {
els[i].className = els[i].className.replace('suite hidden', 'suite');
}
}
/**
* Set `el` text to `str`.
*/
function text(el, str) {
if (el.textContent) {
el.textContent = str;
} else {
el.innerText = str;
}
}
/**
* Listen on `event` with callback `fn`.
*/
function on(el, event, fn) {
if (el.addEventListener) {
el.addEventListener(event, fn, false);
} else {
el.attachEvent('on' + event, fn);
}
}
}); // module: reporters/html.js
require.register("reporters/index.js", function(module, exports, require){
exports.Base = require('./base');
exports.Dot = require('./dot');
exports.Doc = require('./doc');
exports.TAP = require('./tap');
exports.JSON = require('./json');
exports.HTML = require('./html');
exports.List = require('./list');
exports.Min = require('./min');
exports.Spec = require('./spec');
exports.Nyan = require('./nyan');
exports.XUnit = require('./xunit');
exports.Markdown = require('./markdown');
exports.Progress = require('./progress');
exports.Landing = require('./landing');
exports.JSONCov = require('./json-cov');
exports.HTMLCov = require('./html-cov');
exports.JSONStream = require('./json-stream');
exports.Teamcity = require('./teamcity');
}); // module: reporters/index.js
require.register("reporters/json-cov.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base');
/**
* Expose `JSONCov`.
*/
exports = module.exports = JSONCov;
/**
* Initialize a new `JsCoverage` reporter.
*
* @param {Runner} runner
* @param {Boolean} output
* @api public
*/
function JSONCov(runner, output) {
var self = this
, output = 1 == arguments.length ? true : output;
Base.call(this, runner);
var tests = []
, failures = []
, passes = [];
runner.on('test end', function(test){
tests.push(test);
});
runner.on('pass', function(test){
passes.push(test);
});
runner.on('fail', function(test){
failures.push(test);
});
runner.on('end', function(){
var cov = global._$jscoverage || {};
var result = self.cov = map(cov);
result.stats = self.stats;
result.tests = tests.map(clean);
result.failures = failures.map(clean);
result.passes = passes.map(clean);
if (!output) return;
process.stdout.write(JSON.stringify(result, null, 2 ));
});
}
/**
* Map jscoverage data to a JSON structure
* suitable for reporting.
*
* @param {Object} cov
* @return {Object}
* @api private
*/
function map(cov) {
var ret = {
instrumentation: 'node-jscoverage'
, sloc: 0
, hits: 0
, misses: 0
, coverage: 0
, files: []
};
for (var filename in cov) {
var data = coverage(filename, cov[filename]);
ret.files.push(data);
ret.hits += data.hits;
ret.misses += data.misses;
ret.sloc += data.sloc;
}
ret.files.sort(function(a, b) {
return a.filename.localeCompare(b.filename);
});
if (ret.sloc > 0) {
ret.coverage = (ret.hits / ret.sloc) * 100;
}
return ret;
};
/**
* Map jscoverage data for a single source file
* to a JSON structure suitable for reporting.
*
* @param {String} filename name of the source file
* @param {Object} data jscoverage coverage data
* @return {Object}
* @api private
*/
function coverage(filename, data) {
var ret = {
filename: filename,
coverage: 0,
hits: 0,
misses: 0,
sloc: 0,
source: {}
};
data.source.forEach(function(line, num){
num++;
if (data[num] === 0) {
ret.misses++;
ret.sloc++;
} else if (data[num] !== undefined) {
ret.hits++;
ret.sloc++;
}
ret.source[num] = {
source: line
, coverage: data[num] === undefined
? ''
: data[num]
};
});
ret.coverage = ret.hits / ret.sloc * 100;
return ret;
}
/**
* Return a plain-object representation of `test`
* free of cyclic properties etc.
*
* @param {Object} test
* @return {Object}
* @api private
*/
function clean(test) {
return {
title: test.title
, fullTitle: test.fullTitle()
, duration: test.duration
}
}
}); // module: reporters/json-cov.js
require.register("reporters/json-stream.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, color = Base.color;
/**
* Expose `List`.
*/
exports = module.exports = List;
/**
* Initialize a new `List` test reporter.
*
* @param {Runner} runner
* @api public
*/
function List(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, total = runner.total;
runner.on('start', function(){
console.log(JSON.stringify(['start', { total: total }]));
});
runner.on('pass', function(test){
console.log(JSON.stringify(['pass', clean(test)]));
});
runner.on('fail', function(test, err){
console.log(JSON.stringify(['fail', clean(test)]));
});
runner.on('end', function(){
process.stdout.write(JSON.stringify(['end', self.stats]));
});
}
/**
* Return a plain-object representation of `test`
* free of cyclic properties etc.
*
* @param {Object} test
* @return {Object}
* @api private
*/
function clean(test) {
return {
title: test.title
, fullTitle: test.fullTitle()
, duration: test.duration
}
}
}); // module: reporters/json-stream.js
require.register("reporters/json.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
/**
* Expose `JSON`.
*/
exports = module.exports = JSONReporter;
/**
* Initialize a new `JSON` reporter.
*
* @param {Runner} runner
* @api public
*/
function JSONReporter(runner) {
var self = this;
Base.call(this, runner);
var tests = []
, failures = []
, passes = [];
runner.on('test end', function(test){
tests.push(test);
});
runner.on('pass', function(test){
passes.push(test);
});
runner.on('fail', function(test){
failures.push(test);
});
runner.on('end', function(){
var obj = {
stats: self.stats
, tests: tests.map(clean)
, failures: failures.map(clean)
, passes: passes.map(clean)
};
process.stdout.write(JSON.stringify(obj, null, 2));
});
}
/**
* Return a plain-object representation of `test`
* free of cyclic properties etc.
*
* @param {Object} test
* @return {Object}
* @api private
*/
function clean(test) {
return {
title: test.title
, fullTitle: test.fullTitle()
, duration: test.duration
}
}
}); // module: reporters/json.js
require.register("reporters/landing.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
/**
* Expose `Landing`.
*/
exports = module.exports = Landing;
/**
* Airplane color.
*/
Base.colors.plane = 0;
/**
* Airplane crash color.
*/
Base.colors['plane crash'] = 31;
/**
* Runway color.
*/
Base.colors.runway = 90;
/**
* Initialize a new `Landing` reporter.
*
* @param {Runner} runner
* @api public
*/
function Landing(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, width = Base.window.width * .75 | 0
, total = runner.total
, stream = process.stdout
, plane = color('plane', '✈')
, crashed = -1
, n = 0;
function runway() {
var buf = Array(width).join('-');
return ' ' + color('runway', buf);
}
runner.on('start', function(){
stream.write('\n ');
cursor.hide();
});
runner.on('test end', function(test){
// check if the plane crashed
var col = -1 == crashed
? width * ++n / total | 0
: crashed;
// show the crash
if ('failed' == test.state) {
plane = color('plane crash', '✈');
crashed = col;
}
// render landing strip
stream.write('\u001b[4F\n\n');
stream.write(runway());
stream.write('\n ');
stream.write(color('runway', Array(col).join('⋅')));
stream.write(plane)
stream.write(color('runway', Array(width - col).join('⋅') + '\n'));
stream.write(runway());
stream.write('\u001b[0m');
});
runner.on('end', function(){
cursor.show();
console.log();
self.epilogue();
});
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
Landing.prototype = new F;
Landing.prototype.constructor = Landing;
}); // module: reporters/landing.js
require.register("reporters/list.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
/**
* Expose `List`.
*/
exports = module.exports = List;
/**
* Initialize a new `List` test reporter.
*
* @param {Runner} runner
* @api public
*/
function List(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, n = 0;
runner.on('start', function(){
console.log();
});
runner.on('test', function(test){
process.stdout.write(color('pass', ' ' + test.fullTitle() + ': '));
});
runner.on('pending', function(test){
var fmt = color('checkmark', ' -')
+ color('pending', ' %s');
console.log(fmt, test.fullTitle());
});
runner.on('pass', function(test){
var fmt = color('checkmark', ' '+Base.symbols.dot)
+ color('pass', ' %s: ')
+ color(test.speed, '%dms');
cursor.CR();
console.log(fmt, test.fullTitle(), test.duration);
});
runner.on('fail', function(test, err){
cursor.CR();
console.log(color('fail', ' %d) %s'), ++n, test.fullTitle());
});
runner.on('end', self.epilogue.bind(self));
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
List.prototype = new F;
List.prototype.constructor = List;
}); // module: reporters/list.js
require.register("reporters/markdown.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, utils = require('../utils');
/**
* Expose `Markdown`.
*/
exports = module.exports = Markdown;
/**
* Initialize a new `Markdown` reporter.
*
* @param {Runner} runner
* @api public
*/
function Markdown(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, level = 0
, buf = '';
function title(str) {
return Array(level).join('#') + ' ' + str;
}
function indent() {
return Array(level).join(' ');
}
function mapTOC(suite, obj) {
var ret = obj;
obj = obj[suite.title] = obj[suite.title] || { suite: suite };
suite.suites.forEach(function(suite){
mapTOC(suite, obj);
});
return ret;
}
function stringifyTOC(obj, level) {
++level;
var buf = '';
var link;
for (var key in obj) {
if ('suite' == key) continue;
if (key) link = ' - [' + key + '](#' + utils.slug(obj[key].suite.fullTitle()) + ')\n';
if (key) buf += Array(level).join(' ') + link;
buf += stringifyTOC(obj[key], level);
}
--level;
return buf;
}
function generateTOC(suite) {
var obj = mapTOC(suite, {});
return stringifyTOC(obj, 0);
}
generateTOC(runner.suite);
runner.on('suite', function(suite){
++level;
var slug = utils.slug(suite.fullTitle());
buf += '' + '\n';
buf += title(suite.title) + '\n';
});
runner.on('suite end', function(suite){
--level;
});
runner.on('pass', function(test){
var code = utils.clean(test.fn.toString());
buf += test.title + '.\n';
buf += '\n```js\n';
buf += code + '\n';
buf += '```\n\n';
});
runner.on('end', function(){
process.stdout.write('# TOC\n');
process.stdout.write(generateTOC(runner.suite));
process.stdout.write(buf);
});
}
}); // module: reporters/markdown.js
require.register("reporters/min.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base');
/**
* Expose `Min`.
*/
exports = module.exports = Min;
/**
* Initialize a new `Min` minimal test reporter (best used with --watch).
*
* @param {Runner} runner
* @api public
*/
function Min(runner) {
Base.call(this, runner);
runner.on('start', function(){
// clear screen
process.stdout.write('\u001b[2J');
// set cursor position
process.stdout.write('\u001b[1;3H');
});
runner.on('end', this.epilogue.bind(this));
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
Min.prototype = new F;
Min.prototype.constructor = Min;
}); // module: reporters/min.js
require.register("reporters/nyan.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, color = Base.color;
/**
* Expose `Dot`.
*/
exports = module.exports = NyanCat;
/**
* Initialize a new `Dot` matrix test reporter.
*
* @param {Runner} runner
* @api public
*/
function NyanCat(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, width = Base.window.width * .75 | 0
, rainbowColors = this.rainbowColors = self.generateColors()
, colorIndex = this.colorIndex = 0
, numerOfLines = this.numberOfLines = 4
, trajectories = this.trajectories = [[], [], [], []]
, nyanCatWidth = this.nyanCatWidth = 11
, trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth)
, scoreboardWidth = this.scoreboardWidth = 5
, tick = this.tick = 0
, n = 0;
runner.on('start', function(){
Base.cursor.hide();
self.draw('start');
});
runner.on('pending', function(test){
self.draw('pending');
});
runner.on('pass', function(test){
self.draw('pass');
});
runner.on('fail', function(test, err){
self.draw('fail');
});
runner.on('end', function(){
Base.cursor.show();
for (var i = 0; i < self.numberOfLines; i++) write('\n');
self.epilogue();
});
}
/**
* Draw the nyan cat with runner `status`.
*
* @param {String} status
* @api private
*/
NyanCat.prototype.draw = function(status){
this.appendRainbow();
this.drawScoreboard();
this.drawRainbow();
this.drawNyanCat(status);
this.tick = !this.tick;
};
/**
* Draw the "scoreboard" showing the number
* of passes, failures and pending tests.
*
* @api private
*/
NyanCat.prototype.drawScoreboard = function(){
var stats = this.stats;
var colors = Base.colors;
function draw(color, n) {
write(' ');
write('\u001b[' + color + 'm' + n + '\u001b[0m');
write('\n');
}
draw(colors.green, stats.passes);
draw(colors.fail, stats.failures);
draw(colors.pending, stats.pending);
write('\n');
this.cursorUp(this.numberOfLines);
};
/**
* Append the rainbow.
*
* @api private
*/
NyanCat.prototype.appendRainbow = function(){
var segment = this.tick ? '_' : '-';
var rainbowified = this.rainbowify(segment);
for (var index = 0; index < this.numberOfLines; index++) {
var trajectory = this.trajectories[index];
if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift();
trajectory.push(rainbowified);
}
};
/**
* Draw the rainbow.
*
* @api private
*/
NyanCat.prototype.drawRainbow = function(){
var self = this;
this.trajectories.forEach(function(line, index) {
write('\u001b[' + self.scoreboardWidth + 'C');
write(line.join(''));
write('\n');
});
this.cursorUp(this.numberOfLines);
};
/**
* Draw the nyan cat with `status`.
*
* @param {String} status
* @api private
*/
NyanCat.prototype.drawNyanCat = function(status) {
var self = this;
var startWidth = this.scoreboardWidth + this.trajectories[0].length;
var color = '\u001b[' + startWidth + 'C';
var padding = '';
write(color);
write('_,------,');
write('\n');
write(color);
padding = self.tick ? ' ' : ' ';
write('_|' + padding + '/\\_/\\ ');
write('\n');
write(color);
padding = self.tick ? '_' : '__';
var tail = self.tick ? '~' : '^';
var face;
switch (status) {
case 'pass':
face = '( ^ .^)';
break;
case 'fail':
face = '( o .o)';
break;
default:
face = '( - .-)';
}
write(tail + '|' + padding + face + ' ');
write('\n');
write(color);
padding = self.tick ? ' ' : ' ';
write(padding + '"" "" ');
write('\n');
this.cursorUp(this.numberOfLines);
};
/**
* Move cursor up `n`.
*
* @param {Number} n
* @api private
*/
NyanCat.prototype.cursorUp = function(n) {
write('\u001b[' + n + 'A');
};
/**
* Move cursor down `n`.
*
* @param {Number} n
* @api private
*/
NyanCat.prototype.cursorDown = function(n) {
write('\u001b[' + n + 'B');
};
/**
* Generate rainbow colors.
*
* @return {Array}
* @api private
*/
NyanCat.prototype.generateColors = function(){
var colors = [];
for (var i = 0; i < (6 * 7); i++) {
var pi3 = Math.floor(Math.PI / 3);
var n = (i * (1.0 / 6));
var r = Math.floor(3 * Math.sin(n) + 3);
var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3);
var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3);
colors.push(36 * r + 6 * g + b + 16);
}
return colors;
};
/**
* Apply rainbow to the given `str`.
*
* @param {String} str
* @return {String}
* @api private
*/
NyanCat.prototype.rainbowify = function(str){
var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length];
this.colorIndex += 1;
return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m';
};
/**
* Stdout helper.
*/
function write(string) {
process.stdout.write(string);
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
NyanCat.prototype = new F;
NyanCat.prototype.constructor = NyanCat;
}); // module: reporters/nyan.js
require.register("reporters/progress.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
/**
* Expose `Progress`.
*/
exports = module.exports = Progress;
/**
* General progress bar color.
*/
Base.colors.progress = 90;
/**
* Initialize a new `Progress` bar test reporter.
*
* @param {Runner} runner
* @param {Object} options
* @api public
*/
function Progress(runner, options) {
Base.call(this, runner);
var self = this
, options = options || {}
, stats = this.stats
, width = Base.window.width * .50 | 0
, total = runner.total
, complete = 0
, max = Math.max;
// default chars
options.open = options.open || '[';
options.complete = options.complete || '▬';
options.incomplete = options.incomplete || Base.symbols.dot;
options.close = options.close || ']';
options.verbose = false;
// tests started
runner.on('start', function(){
console.log();
cursor.hide();
});
// tests complete
runner.on('test end', function(){
complete++;
var incomplete = total - complete
, percent = complete / total
, n = width * percent | 0
, i = width - n;
cursor.CR();
process.stdout.write('\u001b[J');
process.stdout.write(color('progress', ' ' + options.open));
process.stdout.write(Array(n).join(options.complete));
process.stdout.write(Array(i).join(options.incomplete));
process.stdout.write(color('progress', options.close));
if (options.verbose) {
process.stdout.write(color('progress', ' ' + complete + ' of ' + total));
}
});
// tests are complete, output some stats
// and the failures if any
runner.on('end', function(){
cursor.show();
console.log();
self.epilogue();
});
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
Progress.prototype = new F;
Progress.prototype.constructor = Progress;
}); // module: reporters/progress.js
require.register("reporters/spec.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
/**
* Expose `Spec`.
*/
exports = module.exports = Spec;
/**
* Initialize a new `Spec` test reporter.
*
* @param {Runner} runner
* @api public
*/
function Spec(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, indents = 0
, n = 0;
function indent() {
return Array(indents).join(' ')
}
runner.on('start', function(){
console.log();
});
runner.on('suite', function(suite){
++indents;
console.log(color('suite', '%s%s'), indent(), suite.title);
});
runner.on('suite end', function(suite){
--indents;
if (1 == indents) console.log();
});
runner.on('test', function(test){
process.stdout.write(indent() + color('pass', ' ◦ ' + test.title + ': '));
});
runner.on('pending', function(test){
var fmt = indent() + color('pending', ' - %s');
console.log(fmt, test.title);
});
runner.on('pass', function(test){
if ('fast' == test.speed) {
var fmt = indent()
+ color('checkmark', ' ' + Base.symbols.ok)
+ color('pass', ' %s ');
cursor.CR();
console.log(fmt, test.title);
} else {
var fmt = indent()
+ color('checkmark', ' ' + Base.symbols.ok)
+ color('pass', ' %s ')
+ color(test.speed, '(%dms)');
cursor.CR();
console.log(fmt, test.title, test.duration);
}
});
runner.on('fail', function(test, err){
cursor.CR();
console.log(indent() + color('fail', ' %d) %s'), ++n, test.title);
});
runner.on('end', self.epilogue.bind(self));
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
Spec.prototype = new F;
Spec.prototype.constructor = Spec;
}); // module: reporters/spec.js
require.register("reporters/tap.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
/**
* Expose `TAP`.
*/
exports = module.exports = TAP;
/**
* Initialize a new `TAP` reporter.
*
* @param {Runner} runner
* @api public
*/
function TAP(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, n = 1
, passes = 0
, failures = 0;
runner.on('start', function(){
var total = runner.grepTotal(runner.suite);
console.log('%d..%d', 1, total);
});
runner.on('test end', function(){
++n;
});
runner.on('pending', function(test){
console.log('ok %d %s # SKIP -', n, title(test));
});
runner.on('pass', function(test){
passes++;
console.log('ok %d %s', n, title(test));
});
runner.on('fail', function(test, err){
failures++;
console.log('not ok %d %s', n, title(test));
if (err.stack) console.log(err.stack.replace(/^/gm, ' '));
});
runner.on('end', function(){
console.log('# tests ' + (passes + failures));
console.log('# pass ' + passes);
console.log('# fail ' + failures);
});
}
/**
* Return a TAP-safe title of `test`
*
* @param {Object} test
* @return {String}
* @api private
*/
function title(test) {
return test.fullTitle().replace(/#/g, '');
}
}); // module: reporters/tap.js
require.register("reporters/teamcity.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base');
/**
* Expose `Teamcity`.
*/
exports = module.exports = Teamcity;
/**
* Initialize a new `Teamcity` reporter.
*
* @param {Runner} runner
* @api public
*/
function Teamcity(runner) {
Base.call(this, runner);
var stats = this.stats;
runner.on('start', function() {
console.log("##teamcity[testSuiteStarted name='mocha.suite']");
});
runner.on('test', function(test) {
console.log("##teamcity[testStarted name='" + escape(test.fullTitle()) + "']");
});
runner.on('fail', function(test, err) {
console.log("##teamcity[testFailed name='" + escape(test.fullTitle()) + "' message='" + escape(err.message) + "']");
});
runner.on('pending', function(test) {
console.log("##teamcity[testIgnored name='" + escape(test.fullTitle()) + "' message='pending']");
});
runner.on('test end', function(test) {
console.log("##teamcity[testFinished name='" + escape(test.fullTitle()) + "' duration='" + test.duration + "']");
});
runner.on('end', function() {
console.log("##teamcity[testSuiteFinished name='mocha.suite' duration='" + stats.duration + "']");
});
}
/**
* Escape the given `str`.
*/
function escape(str) {
return str
.replace(/\|/g, "||")
.replace(/\n/g, "|n")
.replace(/\r/g, "|r")
.replace(/\[/g, "|[")
.replace(/\]/g, "|]")
.replace(/\u0085/g, "|x")
.replace(/\u2028/g, "|l")
.replace(/\u2029/g, "|p")
.replace(/'/g, "|'");
}
}); // module: reporters/teamcity.js
require.register("reporters/xunit.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Base = require('./base')
, utils = require('../utils')
, escape = utils.escape;
/**
* Save timer references to avoid Sinon interfering (see GH-237).
*/
var Date = global.Date
, setTimeout = global.setTimeout
, setInterval = global.setInterval
, clearTimeout = global.clearTimeout
, clearInterval = global.clearInterval;
/**
* Expose `XUnit`.
*/
exports = module.exports = XUnit;
/**
* Initialize a new `XUnit` reporter.
*
* @param {Runner} runner
* @api public
*/
function XUnit(runner) {
Base.call(this, runner);
var stats = this.stats
, tests = []
, self = this;
runner.on('pass', function(test){
tests.push(test);
});
runner.on('fail', function(test){
tests.push(test);
});
runner.on('end', function(){
console.log(tag('testsuite', {
name: 'Mocha Tests'
, tests: stats.tests
, failures: stats.failures
, errors: stats.failures
, skip: stats.tests - stats.failures - stats.passes
, timestamp: (new Date).toUTCString()
, time: stats.duration / 1000
}, false));
tests.forEach(test);
console.log('');
});
}
/**
* Inherit from `Base.prototype`.
*/
function F(){};
F.prototype = Base.prototype;
XUnit.prototype = new F;
XUnit.prototype.constructor = XUnit;
/**
* Output tag for the given `test.`
*/
function test(test) {
var attrs = {
classname: test.parent.fullTitle()
, name: test.title
, time: test.duration / 1000
};
if ('failed' == test.state) {
var err = test.err;
attrs.message = escape(err.message);
console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack))));
} else if (test.pending) {
console.log(tag('testcase', attrs, false, tag('skipped', {}, true)));
} else {
console.log(tag('testcase', attrs, true) );
}
}
/**
* HTML tag helper.
*/
function tag(name, attrs, close, content) {
var end = close ? '/>' : '>'
, pairs = []
, tag;
for (var key in attrs) {
pairs.push(key + '="' + escape(attrs[key]) + '"');
}
tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end;
if (content) tag += content + '' + name + end;
return tag;
}
/**
* Return cdata escaped CDATA `str`.
*/
function cdata(str) {
return '';
}
}); // module: reporters/xunit.js
require.register("runnable.js", function(module, exports, require){
/**
* Module dependencies.
*/
var EventEmitter = require('browser/events').EventEmitter
, debug = require('browser/debug')('mocha:runnable')
, milliseconds = require('./ms');
/**
* Save timer references to avoid Sinon interfering (see GH-237).
*/
var Date = global.Date
, setTimeout = global.setTimeout
, setInterval = global.setInterval
, clearTimeout = global.clearTimeout
, clearInterval = global.clearInterval;
/**
* Object#toString().
*/
var toString = Object.prototype.toString;
/**
* Expose `Runnable`.
*/
module.exports = Runnable;
/**
* Initialize a new `Runnable` with the given `title` and callback `fn`.
*
* @param {String} title
* @param {Function} fn
* @api private
*/
function Runnable(title, fn) {
this.title = title;
this.fn = fn;
this.async = fn && fn.length;
this.sync = ! this.async;
this._timeout = 2000;
this._slow = 75;
this.timedOut = false;
}
/**
* Inherit from `EventEmitter.prototype`.
*/
function F(){};
F.prototype = EventEmitter.prototype;
Runnable.prototype = new F;
Runnable.prototype.constructor = Runnable;
/**
* Set & get timeout `ms`.
*
* @param {Number|String} ms
* @return {Runnable|Number} ms or self
* @api private
*/
Runnable.prototype.timeout = function(ms){
if (0 == arguments.length) return this._timeout;
if ('string' == typeof ms) ms = milliseconds(ms);
debug('timeout %d', ms);
this._timeout = ms;
if (this.timer) this.resetTimeout();
return this;
};
/**
* Set & get slow `ms`.
*
* @param {Number|String} ms
* @return {Runnable|Number} ms or self
* @api private
*/
Runnable.prototype.slow = function(ms){
if (0 === arguments.length) return this._slow;
if ('string' == typeof ms) ms = milliseconds(ms);
debug('timeout %d', ms);
this._slow = ms;
return this;
};
/**
* Return the full title generated by recursively
* concatenating the parent's full title.
*
* @return {String}
* @api public
*/
Runnable.prototype.fullTitle = function(){
return this.parent.fullTitle() + ' ' + this.title;
};
/**
* Clear the timeout.
*
* @api private
*/
Runnable.prototype.clearTimeout = function(){
clearTimeout(this.timer);
};
/**
* Inspect the runnable void of private properties.
*
* @return {String}
* @api private
*/
Runnable.prototype.inspect = function(){
return JSON.stringify(this, function(key, val){
if ('_' == key[0]) return;
if ('parent' == key) return '#';
if ('ctx' == key) return '#';
return val;
}, 2);
};
/**
* Reset the timeout.
*
* @api private
*/
Runnable.prototype.resetTimeout = function(){
var self = this
, ms = this.timeout();
this.clearTimeout();
if (ms) {
this.timer = setTimeout(function(){
self.callback(new Error('timeout of ' + ms + 'ms exceeded'));
self.timedOut = true;
}, ms);
}
};
/**
* Run the test and invoke `fn(err)`.
*
* @param {Function} fn
* @api private
*/
Runnable.prototype.run = function(fn){
var self = this
, ms = this.timeout()
, start = new Date
, ctx = this.ctx
, finished
, emitted;
if (ctx) ctx.runnable(this);
// timeout
if (this.async) {
if (ms) {
this.timer = setTimeout(function(){
done(new Error('timeout of ' + ms + 'ms exceeded'));
self.timedOut = true;
}, ms);
}
}
// called multiple times
function multiple(err) {
if (emitted) return;
emitted = true;
self.emit('error', err || new Error('done() called multiple times'));
}
// finished
function done(err) {
if (self.timedOut) return;
if (finished) return multiple(err);
self.clearTimeout();
self.duration = new Date - start;
finished = true;
fn(err);
}
// for .resetTimeout()
this.callback = done;
// async
if (this.async) {
try {
this.fn.call(ctx, function(err){
if (err instanceof Error || toString.call(err) === "[object Error]") return done(err);
if (null != err) return done(new Error('done() invoked with non-Error: ' + err));
done();
});
} catch (err) {
done(err);
}
return;
}
if (this.asyncOnly) {
return done(new Error('--async-only option in use without declaring `done()`'));
}
// sync
try {
if (!this.pending) this.fn.call(ctx);
this.duration = new Date - start;
fn();
} catch (err) {
fn(err);
}
};
}); // module: runnable.js
require.register("runner.js", function(module, exports, require){
/**
* Module dependencies.
*/
var EventEmitter = require('browser/events').EventEmitter
, debug = require('browser/debug')('mocha:runner')
, Test = require('./test')
, utils = require('./utils')
, filter = utils.filter
, keys = utils.keys;
/**
* Non-enumerable globals.
*/
var globals = [
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'XMLHttpRequest',
'Date'
];
/**
* Expose `Runner`.
*/
module.exports = Runner;
/**
* Initialize a `Runner` for the given `suite`.
*
* Events:
*
* - `start` execution started
* - `end` execution complete
* - `suite` (suite) test suite execution started
* - `suite end` (suite) all tests (and sub-suites) have finished
* - `test` (test) test execution started
* - `test end` (test) test completed
* - `hook` (hook) hook execution started
* - `hook end` (hook) hook complete
* - `pass` (test) test passed
* - `fail` (test, err) test failed
*
* @api public
*/
function Runner(suite) {
var self = this;
this._globals = [];
this.suite = suite;
this.total = suite.total();
this.failures = 0;
this.on('test end', function(test){ self.checkGlobals(test); });
this.on('hook end', function(hook){ self.checkGlobals(hook); });
this.grep(/.*/);
this.globals(this.globalProps().concat(['errno']));
}
/**
* Wrapper for setImmediate, process.nextTick, or browser polyfill.
*
* @param {Function} fn
* @api private
*/
Runner.immediately = global.setImmediate || process.nextTick;
/**
* Inherit from `EventEmitter.prototype`.
*/
function F(){};
F.prototype = EventEmitter.prototype;
Runner.prototype = new F;
Runner.prototype.constructor = Runner;
/**
* Run tests with full titles matching `re`. Updates runner.total
* with number of tests matched.
*
* @param {RegExp} re
* @param {Boolean} invert
* @return {Runner} for chaining
* @api public
*/
Runner.prototype.grep = function(re, invert){
debug('grep %s', re);
this._grep = re;
this._invert = invert;
this.total = this.grepTotal(this.suite);
return this;
};
/**
* Returns the number of tests matching the grep search for the
* given suite.
*
* @param {Suite} suite
* @return {Number}
* @api public
*/
Runner.prototype.grepTotal = function(suite) {
var self = this;
var total = 0;
suite.eachTest(function(test){
var match = self._grep.test(test.fullTitle());
if (self._invert) match = !match;
if (match) total++;
});
return total;
};
/**
* Return a list of global properties.
*
* @return {Array}
* @api private
*/
Runner.prototype.globalProps = function() {
var props = utils.keys(global);
// non-enumerables
for (var i = 0; i < globals.length; ++i) {
if (~utils.indexOf(props, globals[i])) continue;
props.push(globals[i]);
}
return props;
};
/**
* Allow the given `arr` of globals.
*
* @param {Array} arr
* @return {Runner} for chaining
* @api public
*/
Runner.prototype.globals = function(arr){
if (0 == arguments.length) return this._globals;
debug('globals %j', arr);
utils.forEach(arr, function(arr){
this._globals.push(arr);
}, this);
return this;
};
/**
* Check for global variable leaks.
*
* @api private
*/
Runner.prototype.checkGlobals = function(test){
if (this.ignoreLeaks) return;
var ok = this._globals;
var globals = this.globalProps();
var isNode = process.kill;
var leaks;
// check length - 2 ('errno' and 'location' globals)
if (isNode && 1 == ok.length - globals.length) return
else if (2 == ok.length - globals.length) return;
leaks = filterLeaks(ok, globals);
this._globals = this._globals.concat(leaks);
if (leaks.length > 1) {
this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + ''));
} else if (leaks.length) {
this.fail(test, new Error('global leak detected: ' + leaks[0]));
}
};
/**
* Fail the given `test`.
*
* @param {Test} test
* @param {Error} err
* @api private
*/
Runner.prototype.fail = function(test, err){
++this.failures;
test.state = 'failed';
if ('string' == typeof err) {
err = new Error('the string "' + err + '" was thrown, throw an Error :)');
}
this.emit('fail', test, err);
};
/**
* Fail the given `hook` with `err`.
*
* Hook failures (currently) hard-end due
* to that fact that a failing hook will
* surely cause subsequent tests to fail,
* causing jumbled reporting.
*
* @param {Hook} hook
* @param {Error} err
* @api private
*/
Runner.prototype.failHook = function(hook, err){
this.fail(hook, err);
this.emit('end');
};
/**
* Run hook `name` callbacks and then invoke `fn()`.
*
* @param {String} name
* @param {Function} function
* @api private
*/
Runner.prototype.hook = function(name, fn){
var suite = this.suite
, hooks = suite['_' + name]
, self = this
, timer;
function next(i) {
var hook = hooks[i];
if (!hook) return fn();
self.currentRunnable = hook;
self.emit('hook', hook);
hook.on('error', function(err){
self.failHook(hook, err);
});
hook.run(function(err){
hook.removeAllListeners('error');
var testError = hook.error();
if (testError) self.fail(self.test, testError);
if (err) return self.failHook(hook, err);
self.emit('hook end', hook);
next(++i);
});
}
Runner.immediately(function(){
next(0);
});
};
/**
* Run hook `name` for the given array of `suites`
* in order, and callback `fn(err)`.
*
* @param {String} name
* @param {Array} suites
* @param {Function} fn
* @api private
*/
Runner.prototype.hooks = function(name, suites, fn){
var self = this
, orig = this.suite;
function next(suite) {
self.suite = suite;
if (!suite) {
self.suite = orig;
return fn();
}
self.hook(name, function(err){
if (err) {
self.suite = orig;
return fn(err);
}
next(suites.pop());
});
}
next(suites.pop());
};
/**
* Run hooks from the top level down.
*
* @param {String} name
* @param {Function} fn
* @api private
*/
Runner.prototype.hookUp = function(name, fn){
var suites = [this.suite].concat(this.parents()).reverse();
this.hooks(name, suites, fn);
};
/**
* Run hooks from the bottom up.
*
* @param {String} name
* @param {Function} fn
* @api private
*/
Runner.prototype.hookDown = function(name, fn){
var suites = [this.suite].concat(this.parents());
this.hooks(name, suites, fn);
};
/**
* Return an array of parent Suites from
* closest to furthest.
*
* @return {Array}
* @api private
*/
Runner.prototype.parents = function(){
var suite = this.suite
, suites = [];
while (suite = suite.parent) suites.push(suite);
return suites;
};
/**
* Run the current test and callback `fn(err)`.
*
* @param {Function} fn
* @api private
*/
Runner.prototype.runTest = function(fn){
var test = this.test
, self = this;
if (this.asyncOnly) test.asyncOnly = true;
try {
test.on('error', function(err){
self.fail(test, err);
});
test.run(fn);
} catch (err) {
fn(err);
}
};
/**
* Run tests in the given `suite` and invoke
* the callback `fn()` when complete.
*
* @param {Suite} suite
* @param {Function} fn
* @api private
*/
Runner.prototype.runTests = function(suite, fn){
var self = this
, tests = suite.tests.slice()
, test;
function next(err) {
// if we bail after first err
if (self.failures && suite._bail) return fn();
// next test
test = tests.shift();
// all done
if (!test) return fn();
// grep
var match = self._grep.test(test.fullTitle());
if (self._invert) match = !match;
if (!match) return next();
// pending
if (test.pending) {
self.emit('pending', test);
self.emit('test end', test);
return next();
}
// execute test and hook(s)
self.emit('test', self.test = test);
self.hookDown('beforeEach', function(){
self.currentRunnable = self.test;
self.runTest(function(err){
test = self.test;
if (err) {
self.fail(test, err);
self.emit('test end', test);
return self.hookUp('afterEach', next);
}
test.state = 'passed';
self.emit('pass', test);
self.emit('test end', test);
self.hookUp('afterEach', next);
});
});
}
this.next = next;
next();
};
/**
* Run the given `suite` and invoke the
* callback `fn()` when complete.
*
* @param {Suite} suite
* @param {Function} fn
* @api private
*/
Runner.prototype.runSuite = function(suite, fn){
var total = this.grepTotal(suite)
, self = this
, i = 0;
debug('run suite %s', suite.fullTitle());
if (!total) return fn();
this.emit('suite', this.suite = suite);
function next() {
var curr = suite.suites[i++];
if (!curr) return done();
self.runSuite(curr, next);
}
function done() {
self.suite = suite;
self.hook('afterAll', function(){
self.emit('suite end', suite);
fn();
});
}
this.hook('beforeAll', function(){
self.runTests(suite, next);
});
};
/**
* Handle uncaught exceptions.
*
* @param {Error} err
* @api private
*/
Runner.prototype.uncaught = function(err){
debug('uncaught exception %s', err.message);
var runnable = this.currentRunnable;
if (!runnable || 'failed' == runnable.state) return;
runnable.clearTimeout();
err.uncaught = true;
this.fail(runnable, err);
// recover from test
if ('test' == runnable.type) {
this.emit('test end', runnable);
this.hookUp('afterEach', this.next);
return;
}
// bail on hooks
this.emit('end');
};
/**
* Run the root suite and invoke `fn(failures)`
* on completion.
*
* @param {Function} fn
* @return {Runner} for chaining
* @api public
*/
Runner.prototype.run = function(fn){
var self = this
, fn = fn || function(){};
function uncaught(err){
self.uncaught(err);
}
debug('start');
// callback
this.on('end', function(){
debug('end');
process.removeListener('uncaughtException', uncaught);
fn(self.failures);
});
// run suites
this.emit('start');
this.runSuite(this.suite, function(){
debug('finished running');
self.emit('end');
});
// uncaught exception
process.on('uncaughtException', uncaught);
return this;
};
/**
* Filter leaks with the given globals flagged as `ok`.
*
* @param {Array} ok
* @param {Array} globals
* @return {Array}
* @api private
*/
function filterLeaks(ok, globals) {
return filter(globals, function(key){
// Firefox and Chrome exposes iframes as index inside the window object
if (/^d+/.test(key)) return false;
var matched = filter(ok, function(ok){
if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]);
// Opera and IE expose global variables for HTML element IDs (issue #243)
if (/^mocha-/.test(key)) return true;
return key == ok;
});
return matched.length == 0 && (!global.navigator || 'onerror' !== key);
});
}
}); // module: runner.js
require.register("suite.js", function(module, exports, require){
/**
* Module dependencies.
*/
var EventEmitter = require('browser/events').EventEmitter
, debug = require('browser/debug')('mocha:suite')
, milliseconds = require('./ms')
, utils = require('./utils')
, Hook = require('./hook');
/**
* Expose `Suite`.
*/
exports = module.exports = Suite;
/**
* Create a new `Suite` with the given `title`
* and parent `Suite`. When a suite with the
* same title is already present, that suite
* is returned to provide nicer reporter
* and more flexible meta-testing.
*
* @param {Suite} parent
* @param {String} title
* @return {Suite}
* @api public
*/
exports.create = function(parent, title){
var suite = new Suite(title, parent.ctx);
suite.parent = parent;
if (parent.pending) suite.pending = true;
title = suite.fullTitle();
parent.addSuite(suite);
return suite;
};
/**
* Initialize a new `Suite` with the given
* `title` and `ctx`.
*
* @param {String} title
* @param {Context} ctx
* @api private
*/
function Suite(title, ctx) {
this.title = title;
this.ctx = ctx;
this.suites = [];
this.tests = [];
this.pending = false;
this._beforeEach = [];
this._beforeAll = [];
this._afterEach = [];
this._afterAll = [];
this.root = !title;
this._timeout = 2000;
this._slow = 75;
this._bail = false;
}
/**
* Inherit from `EventEmitter.prototype`.
*/
function F(){};
F.prototype = EventEmitter.prototype;
Suite.prototype = new F;
Suite.prototype.constructor = Suite;
/**
* Return a clone of this `Suite`.
*
* @return {Suite}
* @api private
*/
Suite.prototype.clone = function(){
var suite = new Suite(this.title);
debug('clone');
suite.ctx = this.ctx;
suite.timeout(this.timeout());
suite.slow(this.slow());
suite.bail(this.bail());
return suite;
};
/**
* Set timeout `ms` or short-hand such as "2s".
*
* @param {Number|String} ms
* @return {Suite|Number} for chaining
* @api private
*/
Suite.prototype.timeout = function(ms){
if (0 == arguments.length) return this._timeout;
if ('string' == typeof ms) ms = milliseconds(ms);
debug('timeout %d', ms);
this._timeout = parseInt(ms, 10);
return this;
};
/**
* Set slow `ms` or short-hand such as "2s".
*
* @param {Number|String} ms
* @return {Suite|Number} for chaining
* @api private
*/
Suite.prototype.slow = function(ms){
if (0 === arguments.length) return this._slow;
if ('string' == typeof ms) ms = milliseconds(ms);
debug('slow %d', ms);
this._slow = ms;
return this;
};
/**
* Sets whether to bail after first error.
*
* @parma {Boolean} bail
* @return {Suite|Number} for chaining
* @api private
*/
Suite.prototype.bail = function(bail){
if (0 == arguments.length) return this._bail;
debug('bail %s', bail);
this._bail = bail;
return this;
};
/**
* Run `fn(test[, done])` before running tests.
*
* @param {Function} fn
* @return {Suite} for chaining
* @api private
*/
Suite.prototype.beforeAll = function(fn){
if (this.pending) return this;
var hook = new Hook('"before all" hook', fn);
hook.parent = this;
hook.timeout(this.timeout());
hook.slow(this.slow());
hook.ctx = this.ctx;
this._beforeAll.push(hook);
this.emit('beforeAll', hook);
return this;
};
/**
* Run `fn(test[, done])` after running tests.
*
* @param {Function} fn
* @return {Suite} for chaining
* @api private
*/
Suite.prototype.afterAll = function(fn){
if (this.pending) return this;
var hook = new Hook('"after all" hook', fn);
hook.parent = this;
hook.timeout(this.timeout());
hook.slow(this.slow());
hook.ctx = this.ctx;
this._afterAll.push(hook);
this.emit('afterAll', hook);
return this;
};
/**
* Run `fn(test[, done])` before each test case.
*
* @param {Function} fn
* @return {Suite} for chaining
* @api private
*/
Suite.prototype.beforeEach = function(fn){
if (this.pending) return this;
var hook = new Hook('"before each" hook', fn);
hook.parent = this;
hook.timeout(this.timeout());
hook.slow(this.slow());
hook.ctx = this.ctx;
this._beforeEach.push(hook);
this.emit('beforeEach', hook);
return this;
};
/**
* Run `fn(test[, done])` after each test case.
*
* @param {Function} fn
* @return {Suite} for chaining
* @api private
*/
Suite.prototype.afterEach = function(fn){
if (this.pending) return this;
var hook = new Hook('"after each" hook', fn);
hook.parent = this;
hook.timeout(this.timeout());
hook.slow(this.slow());
hook.ctx = this.ctx;
this._afterEach.push(hook);
this.emit('afterEach', hook);
return this;
};
/**
* Add a test `suite`.
*
* @param {Suite} suite
* @return {Suite} for chaining
* @api private
*/
Suite.prototype.addSuite = function(suite){
suite.parent = this;
suite.timeout(this.timeout());
suite.slow(this.slow());
suite.bail(this.bail());
this.suites.push(suite);
this.emit('suite', suite);
return this;
};
/**
* Add a `test` to this suite.
*
* @param {Test} test
* @return {Suite} for chaining
* @api private
*/
Suite.prototype.addTest = function(test){
test.parent = this;
test.timeout(this.timeout());
test.slow(this.slow());
test.ctx = this.ctx;
this.tests.push(test);
this.emit('test', test);
return this;
};
/**
* Return the full title generated by recursively
* concatenating the parent's full title.
*
* @return {String}
* @api public
*/
Suite.prototype.fullTitle = function(){
if (this.parent) {
var full = this.parent.fullTitle();
if (full) return full + ' ' + this.title;
}
return this.title;
};
/**
* Return the total number of tests.
*
* @return {Number}
* @api public
*/
Suite.prototype.total = function(){
return utils.reduce(this.suites, function(sum, suite){
return sum + suite.total();
}, 0) + this.tests.length;
};
/**
* Iterates through each suite recursively to find
* all tests. Applies a function in the format
* `fn(test)`.
*
* @param {Function} fn
* @return {Suite}
* @api private
*/
Suite.prototype.eachTest = function(fn){
utils.forEach(this.tests, fn);
utils.forEach(this.suites, function(suite){
suite.eachTest(fn);
});
return this;
};
}); // module: suite.js
require.register("test.js", function(module, exports, require){
/**
* Module dependencies.
*/
var Runnable = require('./runnable');
/**
* Expose `Test`.
*/
module.exports = Test;
/**
* Initialize a new `Test` with the given `title` and callback `fn`.
*
* @param {String} title
* @param {Function} fn
* @api private
*/
function Test(title, fn) {
Runnable.call(this, title, fn);
this.pending = !fn;
this.type = 'test';
}
/**
* Inherit from `Runnable.prototype`.
*/
function F(){};
F.prototype = Runnable.prototype;
Test.prototype = new F;
Test.prototype.constructor = Test;
}); // module: test.js
require.register("utils.js", function(module, exports, require){
/**
* Module dependencies.
*/
var fs = require('browser/fs')
, path = require('browser/path')
, join = path.join
, debug = require('browser/debug')('mocha:watch');
/**
* Ignored directories.
*/
var ignore = ['node_modules', '.git'];
/**
* Escape special characters in the given string of html.
*
* @param {String} html
* @return {String}
* @api private
*/
exports.escape = function(html){
return String(html)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>');
};
/**
* Array#forEach (<=IE8)
*
* @param {Array} array
* @param {Function} fn
* @param {Object} scope
* @api private
*/
exports.forEach = function(arr, fn, scope){
for (var i = 0, l = arr.length; i < l; i++)
fn.call(scope, arr[i], i);
};
/**
* Array#indexOf (<=IE8)
*
* @parma {Array} arr
* @param {Object} obj to find index of
* @param {Number} start
* @api private
*/
exports.indexOf = function(arr, obj, start){
for (var i = start || 0, l = arr.length; i < l; i++) {
if (arr[i] === obj)
return i;
}
return -1;
};
/**
* Array#reduce (<=IE8)
*
* @param {Array} array
* @param {Function} fn
* @param {Object} initial value
* @api private
*/
exports.reduce = function(arr, fn, val){
var rval = val;
for (var i = 0, l = arr.length; i < l; i++) {
rval = fn(rval, arr[i], i, arr);
}
return rval;
};
/**
* Array#filter (<=IE8)
*
* @param {Array} array
* @param {Function} fn
* @api private
*/
exports.filter = function(arr, fn){
var ret = [];
for (var i = 0, l = arr.length; i < l; i++) {
var val = arr[i];
if (fn(val, i, arr)) ret.push(val);
}
return ret;
};
/**
* Object.keys (<=IE8)
*
* @param {Object} obj
* @return {Array} keys
* @api private
*/
exports.keys = Object.keys || function(obj) {
var keys = []
, has = Object.prototype.hasOwnProperty // for `window` on <=IE8
for (var key in obj) {
if (has.call(obj, key)) {
keys.push(key);
}
}
return keys;
};
/**
* Watch the given `files` for changes
* and invoke `fn(file)` on modification.
*
* @param {Array} files
* @param {Function} fn
* @api private
*/
exports.watch = function(files, fn){
var options = { interval: 100 };
files.forEach(function(file){
debug('file %s', file);
fs.watchFile(file, options, function(curr, prev){
if (prev.mtime < curr.mtime) fn(file);
});
});
};
/**
* Ignored files.
*/
function ignored(path){
return !~ignore.indexOf(path);
}
/**
* Lookup files in the given `dir`.
*
* @return {Array}
* @api private
*/
exports.files = function(dir, ret){
ret = ret || [];
fs.readdirSync(dir)
.filter(ignored)
.forEach(function(path){
path = join(dir, path);
if (fs.statSync(path).isDirectory()) {
exports.files(path, ret);
} else if (path.match(/\.(js|coffee)$/)) {
ret.push(path);
}
});
return ret;
};
/**
* Compute a slug from the given `str`.
*
* @param {String} str
* @return {String}
* @api private
*/
exports.slug = function(str){
return str
.toLowerCase()
.replace(/ +/g, '-')
.replace(/[^-\w]/g, '');
};
/**
* Strip the function definition from `str`,
* and re-indent for pre whitespace.
*/
exports.clean = function(str) {
str = str
.replace(/^function *\(.*\) *{/, '')
.replace(/\s+\}$/, '');
var spaces = str.match(/^\n?( *)/)[1].length
, re = new RegExp('^ {' + spaces + '}', 'gm');
str = str.replace(re, '');
return exports.trim(str);
};
/**
* Escape regular expression characters in `str`.
*
* @param {String} str
* @return {String}
* @api private
*/
exports.escapeRegexp = function(str){
return str.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
};
/**
* Trim the given `str`.
*
* @param {String} str
* @return {String}
* @api private
*/
exports.trim = function(str){
return str.replace(/^\s+|\s+$/g, '');
};
/**
* Parse the given `qs`.
*
* @param {String} qs
* @return {Object}
* @api private
*/
exports.parseQuery = function(qs){
return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){
var i = pair.indexOf('=')
, key = pair.slice(0, i)
, val = pair.slice(++i);
obj[key] = decodeURIComponent(val);
return obj;
}, {});
};
/**
* Highlight the given string of `js`.
*
* @param {String} js
* @return {String}
* @api private
*/
function highlight(js) {
return js
.replace(//g, '>')
.replace(/\/\/(.*)/gm, '//$1')
.replace(/('.*?')/gm, '$1')
.replace(/(\d+\.\d+)/gm, '$1')
.replace(/(\d+)/gm, '$1')
.replace(/\bnew *(\w+)/gm, 'new$1')
.replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1')
}
/**
* Highlight the contents of tag `name`.
*
* @param {String} name
* @api private
*/
exports.highlightTags = function(name) {
var code = document.getElementsByTagName(name);
for (var i = 0, len = code.length; i < len; ++i) {
code[i].innerHTML = highlight(code[i].innerHTML);
}
};
}); // module: utils.js
/**
* Save timer references to avoid Sinon interfering (see GH-237).
*/
var Date = window.Date;
var setTimeout = window.setTimeout;
var setInterval = window.setInterval;
var clearTimeout = window.clearTimeout;
var clearInterval = window.clearInterval;
/**
* Node shims.
*
* These are meant only to allow
* mocha.js to run untouched, not
* to allow running node code in
* the browser.
*/
var process = {};
process.exit = function(status){};
process.stdout = {};
global = window;
/**
* Remove uncaughtException listener.
*/
process.removeListener = function(e){
if ('uncaughtException' == e) {
window.onerror = null;
}
};
/**
* Implements uncaughtException listener.
*/
process.on = function(e, fn){
if ('uncaughtException' == e) {
window.onerror = function(err, url, line){
fn(new Error(err + ' (' + url + ':' + line + ')'));
};
}
};
/**
* Expose mocha.
*/
var Mocha = window.Mocha = require('mocha'),
mocha = window.mocha = new Mocha({ reporter: 'html' });
var immediateQueue = []
, immediateTimeout;
function timeslice() {
var immediateStart = new Date().getTime();
while (immediateQueue.length && (new Date().getTime() - immediateStart) < 100) {
immediateQueue.shift()();
}
if (immediateQueue.length) {
immediateTimeout = setTimeout(timeslice, 0);
} else {
immediateTimeout = null;
}
}
/**
* High-performance override of Runner.immediately.
*/
Mocha.Runner.immediately = function(callback) {
immediateQueue.push(callback);
if (!immediateTimeout) {
immediateTimeout = setTimeout(timeslice, 0);
}
};
/**
* Override ui to ensure that the ui functions are initialized.
* Normally this would happen in Mocha.prototype.loadFiles.
*/
mocha.ui = function(ui){
Mocha.prototype.ui.call(this, ui);
this.suite.emit('pre-require', window, null, this);
return this;
};
/**
* Setup mocha with the given setting options.
*/
mocha.setup = function(opts){
if ('string' == typeof opts) opts = { ui: opts };
for (var opt in opts) this[opt](opts[opt]);
return this;
};
/**
* Run mocha, returning the Runner.
*/
mocha.run = function(fn){
var options = mocha.options;
mocha.globals('location');
var query = Mocha.utils.parseQuery(window.location.search || '');
if (query.grep) mocha.grep(query.grep);
if (query.invert) mocha.invert();
return Mocha.prototype.run.call(mocha, function(){
Mocha.utils.highlightTags('code');
if (fn) fn();
});
};
})(); rack-cors-2.0.1/test/unit/ 0000755 0000041 0000041 00000000000 14415577125 015377 5 ustar www-data www-data rack-cors-2.0.1/test/unit/cors_test.rb 0000644 0000041 0000041 00000042026 14415577125 017735 0 ustar www-data www-data # frozen_string_literal: true
require 'minitest/autorun'
require 'rack/test'
require 'mocha/setup'
require 'rack/cors'
require 'ostruct'
Rack::Test::Session.class_eval do
unless defined? :options
def options(uri, params = {}, env = {}, &block)
env = env_for(uri, env.merge(method: 'OPTIONS', params: params))
process_request(uri, env, &block)
end
end
end
Rack::Test::Methods.class_eval do
def_delegator :current_session, :options
end
module MiniTest::Assertions
def assert_cors_success(response)
assert !response.headers['Access-Control-Allow-Origin'].nil?, 'Expected a successful CORS response'
end
def assert_not_cors_success(response)
assert response.headers['Access-Control-Allow-Origin'].nil?, 'Expected a failed CORS response'
end
end
class CaptureResult
def initialize(app, options = {})
@app = app
@result_holder = options[:holder]
end
def call(env)
response = @app.call(env)
@result_holder.cors_result = env[Rack::Cors::RACK_CORS]
response
end
end
class FakeProxy
def initialize(app, _options = {})
@app = app
end
def call(env)
status, headers, body = @app.call(env)
headers['vary'] = %w[Origin User-Agent]
[status, headers, body]
end
end
Rack::MockResponse.infect_an_assertion :assert_cors_success, :must_render_cors_success, :only_one_argument
Rack::MockResponse.infect_an_assertion :assert_not_cors_success, :wont_render_cors_success, :only_one_argument
describe Rack::Cors do
include Rack::Test::Methods
attr_accessor :cors_result
def load_app(name, options = {})
test = self
Rack::Builder.new do
use CaptureResult, holder: test
eval File.read(File.dirname(__FILE__) + "/#{name}.ru")
use FakeProxy if options[:proxy]
map('/') do
run(lambda do |_env|
[200, { 'content-type' => 'text/html' }, ['success']]
end)
end
end
end
let(:app) { load_app('test') }
it 'should support simple CORS request' do
successful_cors_request
_(cors_result).must_be :hit
end
it "should not return CORS headers if Origin header isn't present" do
get '/'
_(last_response).wont_render_cors_success
_(cors_result).wont_be :hit
end
it 'should support OPTIONS CORS request' do
successful_cors_request '/options', method: :options
end
it 'should support regex origins configuration' do
successful_cors_request origin: 'http://192.168.0.1:1234'
end
it 'should support subdomain example' do
successful_cors_request origin: 'http://subdomain.example.com'
end
it 'should support proc origins configuration' do
successful_cors_request '/proc-origin', origin: 'http://10.10.10.10:3000'
end
it 'should support lambda origin configuration' do
successful_cors_request '/lambda-origin', origin: 'http://10.10.10.10:3000'
end
it 'should support proc origins configuration (inverse)' do
cors_request '/proc-origin', origin: 'http://bad.guy'
_(last_response).wont_render_cors_success
end
it 'should not mix up path rules across origins' do
header 'Origin', 'http://10.10.10.10:3000'
get '/' # / is configured in a separate rule block
_(last_response).wont_render_cors_success
end
it 'should support alternative X-Origin header' do
header 'X-Origin', 'http://localhost:3000'
get '/'
_(last_response).must_render_cors_success
end
it 'should support expose header configuration' do
successful_cors_request '/expose_single_header'
_(last_response.headers['Access-Control-Expose-Headers']).must_equal 'expose-test'
end
it 'should support expose multiple header configuration' do
successful_cors_request '/expose_multiple_headers'
_(last_response.headers['Access-Control-Expose-Headers']).must_equal 'expose-test-1, expose-test-2'
end
# Explanation: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
it "should add Vary header if resource matches even if Origin header isn't present" do
get '/'
_(last_response).wont_render_cors_success
_(last_response.headers['Vary']).must_equal 'Origin'
end
it 'should add Vary header based on :vary option' do
successful_cors_request '/vary_test'
_(last_response.headers['Vary']).must_equal 'Origin, Host'
end
it 'decode URL and resolve paths before resource matching' do
header 'Origin', 'http://localhost:3000'
get '/public/a/..%2F..%2Fprivate/stuff'
_(last_response).wont_render_cors_success
end
describe 'with upstream Vary headers' do
let(:app) { load_app('test', { proxy: true }) }
it 'should add to them' do
successful_cors_request '/vary_test'
_(last_response.headers['Vary']).must_equal 'Origin, User-Agent, Host'
end
end
it 'should add Vary header if Access-Control-Allow-Origin header was added and if it is specific' do
successful_cors_request '/', origin: 'http://192.168.0.3:8080'
_(last_response.headers['Access-Control-Allow-Origin']).must_equal 'http://192.168.0.3:8080'
_(last_response.headers['Vary']).wont_be_nil
end
it 'should add Vary header even if Access-Control-Allow-Origin header was added and it is generic (*)' do
successful_cors_request '/public_without_credentials', origin: 'http://192.168.1.3:8080'
_(last_response.headers['Access-Control-Allow-Origin']).must_equal '*'
_(last_response.headers['Vary']).must_equal 'Origin'
end
it 'should support multi allow configurations for the same resource' do
successful_cors_request '/multi-allow-config', origin: 'http://mucho-grande.com'
_(last_response.headers['Access-Control-Allow-Origin']).must_equal 'http://mucho-grande.com'
_(last_response.headers['Vary']).must_equal 'Origin'
successful_cors_request '/multi-allow-config', origin: 'http://192.168.1.3:8080'
_(last_response.headers['Access-Control-Allow-Origin']).must_equal '*'
_(last_response.headers['Vary']).must_equal 'Origin'
end
it 'should not return CORS headers on OPTIONS request if Access-Control-Allow-Origin is not present' do
options '/get-only'
_(last_response.headers['Access-Control-Allow-Origin']).must_be_nil
end
it 'should not apply CORS headers if it does not match conditional on resource' do
header 'Origin', 'http://192.168.0.1:1234'
get '/conditional'
_(last_response).wont_render_cors_success
end
it 'should apply CORS headers if it does match conditional on resource' do
header 'X-OK', '1'
successful_cors_request '/conditional', origin: 'http://192.168.0.1:1234'
end
it 'should not allow everything if Origin is configured as blank string' do
cors_request '/blank-origin', origin: 'http://example.net'
_(last_response).wont_render_cors_success
end
it 'should not allow credentials for public resources' do
successful_cors_request '/public'
_(last_response.headers['Access-Control-Allow-Credentials']).must_be_nil
end
describe 'logging' do
it 'should not log debug messages if debug option is false' do
app = mock
app.stubs(:call).returns(200, {}, [''])
logger = mock
logger.expects(:debug).never
cors = Rack::Cors.new(app, debug: false, logger: logger) {}
cors.send(:debug, {}, 'testing')
end
it 'should log debug messages if debug option is true' do
app = mock
app.stubs(:call).returns(200, {}, [''])
logger = mock
logger.expects(:debug)
cors = Rack::Cors.new(app, debug: true, logger: logger) {}
cors.send(:debug, {}, 'testing')
end
it 'should use rack.logger if available' do
app = mock
app.stubs(:call).returns([200, {}, ['']])
logger = mock
logger.expects(:debug).at_least_once
cors = Rack::Cors.new(app, debug: true) {}
cors.call({ 'rack.logger' => logger, 'HTTP_ORIGIN' => 'test.com' })
end
it 'should use logger proc' do
app = mock
app.stubs(:call).returns([200, {}, ['']])
logger = mock
logger.expects(:debug)
cors = Rack::Cors.new(app, debug: true, logger: proc { logger }) {}
cors.call({ 'HTTP_ORIGIN' => 'test.com' })
end
describe 'with Rails setup' do
after do
::Rails.logger = nil if defined?(::Rails)
end
it 'should use Rails.logger if available' do
app = mock
app.stubs(:call).returns([200, {}, ['']])
logger = mock
logger.expects(:debug)
::Rails = OpenStruct.new(logger: logger)
cors = Rack::Cors.new(app, debug: true) {}
cors.call({ 'HTTP_ORIGIN' => 'test.com' })
end
end
it 'should use Logger if none is set' do
app = mock
app.stubs(:call).returns([200, {}, ['']])
logger = mock
Logger.expects(:new).returns(logger)
logger.expects(:tap).returns(logger)
logger.expects(:debug)
cors = Rack::Cors.new(app, debug: true) {}
cors.call({ 'HTTP_ORIGIN' => 'test.com' })
end
end
describe 'preflight requests' do
it 'should fail if origin is invalid' do
preflight_request('http://allyourdataarebelongtous.com', '/')
_(last_response).wont_render_cors_success
_(cors_result).wont_be :hit
_(cors_result).must_be :preflight
end
it 'should fail if Access-Control-Request-Method is not allowed' do
preflight_request('http://localhost:3000', '/get-only', method: :post)
_(last_response).wont_render_cors_success
_(cors_result.miss_reason).must_equal Rack::Cors::Result::MISS_DENY_METHOD
_(cors_result).wont_be :hit
_(cors_result).must_be :preflight
end
it 'should fail if header is not allowed' do
preflight_request('http://localhost:3000', '/single_header', headers: 'Fooey')
_(last_response).wont_render_cors_success
_(cors_result.miss_reason).must_equal Rack::Cors::Result::MISS_DENY_HEADER
_(cors_result).wont_be :hit
_(cors_result).must_be :preflight
end
it 'should allow any header if headers = :any' do
preflight_request('http://localhost:3000', '/', headers: 'Fooey')
_(last_response).must_render_cors_success
end
it 'should allow any method if methods = :any' do
preflight_request('http://localhost:3000', '/', methods: :any)
_(last_response).must_render_cors_success
end
it 'allows PATCH method' do
preflight_request('http://localhost:3000', '/', methods: [:patch])
_(last_response).must_render_cors_success
end
it 'should allow header case insensitive match' do
preflight_request('http://localhost:3000', '/single_header', headers: 'X-Domain-Token')
_(last_response).must_render_cors_success
end
it 'should allow multiple headers match' do
# Webkit style
preflight_request('http://localhost:3000', '/two_headers', headers: 'X-Requested-With, X-Domain-Token')
_(last_response).must_render_cors_success
# Gecko style
preflight_request('http://localhost:3000', '/two_headers', headers: 'x-requested-with,x-domain-token')
_(last_response).must_render_cors_success
end
it "should allow '*' origins to allow any origin" do
preflight_request('http://locohost:3000', '/public')
_(last_response).must_render_cors_success
_(last_response.headers['Access-Control-Allow-Origin']).must_equal '*'
end
it "should allow '//' resource if match pattern is //*" do
preflight_request('http://localhost:3000', '/wildcard/')
_(last_response).must_render_cors_success
_(last_response.headers['Access-Control-Allow-Origin']).wont_equal nil
end
it "should allow '/' resource if match pattern is //*" do
preflight_request('http://localhost:3000', '/wildcard')
_(last_response).must_render_cors_success
_(last_response.headers['Access-Control-Allow-Origin']).wont_equal nil
end
it "should allow '*' origin to allow any origin, and set '*' if no credentials required" do
preflight_request('http://locohost:3000', '/public_without_credentials')
_(last_response).must_render_cors_success
_(last_response.headers['Access-Control-Allow-Origin']).must_equal '*'
end
it 'should return "file://" as header with "file://" as origin' do
preflight_request('file://', '/')
_(last_response).must_render_cors_success
_(last_response.headers['Access-Control-Allow-Origin']).must_equal 'file://'
end
it 'supports custom protocols in origin' do
preflight_request('custom-protocol://abcdefg', '/')
_(last_response).must_render_cors_success
_(last_response.headers['Access-Control-Allow-Origin']).must_equal 'custom-protocol://abcdefg'
end
describe '' do
let(:app) do
test = self
Rack::Builder.new do
use CaptureResult, holder: test
use Rack::Cors, debug: true, logger: Logger.new(StringIO.new) do
allow do
origins '*'
resource '/', methods: :post
end
end
map('/') do
run ->(_env) { [500, {}, ['FAIL!']] }
end
end
end
it 'should not send failed preflight requests thru the app' do
preflight_request('http://localhost', '/', method: :unsupported)
_(last_response).wont_render_cors_success
_(last_response.status).must_equal 200
_(cors_result).must_be :preflight
_(cors_result).wont_be :hit
_(cors_result.miss_reason).must_equal Rack::Cors::Result::MISS_DENY_METHOD
end
end
end
describe 'with insecure configuration' do
let(:app) { load_app('insecure') }
it 'should raise an error' do
_(proc { cors_request '/public' }).must_raise Rack::Cors::Resource::CorsMisconfigurationError
end
end
describe 'with non HTTP config' do
let(:app) { load_app('non_http') }
it 'should support non http/https origins' do
successful_cors_request '/public', origin: 'content://com.company.app'
end
end
describe 'Rack::Lint' do
def app
@app ||= Rack::Builder.new do
use Rack::Cors
use Rack::Lint
run ->(_env) { [200, { 'content-type' => 'text/html' }, ['hello']] }
end
end
it 'is lint-compliant with non-CORS request' do
get '/'
_(last_response.status).must_equal 200
end
end
describe 'with app overriding CORS header' do
let(:app) do
Rack::Builder.new do
use Rack::Cors, debug: true, logger: Logger.new(StringIO.new) do
allow do
origins '*'
resource '/'
end
end
map('/') do
run ->(_env) { [200, { 'Access-Control-Allow-Origin' => 'http://foo.net' }, ['success']] }
end
end
end
it 'should return app header' do
successful_cors_request origin: 'http://example.net'
_(last_response.headers['Access-Control-Allow-Origin']).must_equal 'http://foo.net'
end
it 'should return original headers if in debug' do
successful_cors_request origin: 'http://example.net'
_(last_response.headers['X-Rack-CORS-Original-Access-Control-Allow-Origin']).must_equal '*'
end
end
describe 'with headers set to nil' do
let(:app) do
Rack::Builder.new do
use Rack::Cors do
allow do
origins '*'
resource '/', headers: nil
end
end
map('/') do
run ->(_env) { [200, { 'content-type' => 'text/html' }, ['hello']] }
end
end
end
it 'should succeed with CORS simple headers' do
preflight_request('http://localhost:3000', '/', headers: 'Accept')
_(last_response).must_render_cors_success
end
end
describe 'with custom allowed headers' do
let(:app) do
Rack::Builder.new do
use Rack::Cors do
allow do
origins '*'
resource '/', headers: []
end
end
map('/') do
run ->(_env) { [200, { 'content-type' => 'text/html' }, ['hello']] }
end
end
end
it 'should succeed with CORS simple headers' do
preflight_request('http://localhost:3000', '/', headers: 'Accept')
_(last_response).must_render_cors_success
preflight_request('http://localhost:3000', '/', headers: 'Accept-Language')
_(last_response).must_render_cors_success
preflight_request('http://localhost:3000', '/', headers: 'Content-Type')
_(last_response).must_render_cors_success
preflight_request('http://localhost:3000', '/', headers: 'Content-Language')
_(last_response).must_render_cors_success
end
end
protected
def cors_request(*args)
path = args.first.is_a?(String) ? args.first : '/'
opts = { method: :get, origin: 'http://localhost:3000' }
opts.merge! args.last if args.last.is_a?(Hash)
header 'origin', opts[:origin]
current_session.__send__ opts[:method], path, {}, test: self
end
def successful_cors_request(*args)
cors_request(*args)
_(last_response).must_render_cors_success
end
def preflight_request(origin, path, opts = {})
header 'Origin', origin
unless opts.key?(:method) && opts[:method].nil?
header 'Access-Control-Request-Method', opts[:method] ? opts[:method].to_s.upcase : 'GET'
end
header 'Access-Control-Request-Headers', opts[:headers] if opts[:headers]
options path
end
end
rack-cors-2.0.1/test/unit/dsl_test.rb 0000644 0000041 0000041 00000004755 14415577125 017560 0 ustar www-data www-data # frozen_string_literal: true
require 'rubygems'
require 'minitest/autorun'
require 'rack/cors'
describe Rack::Cors, 'DSL' do
it 'should support explicit config object dsl mode' do
cors = Rack::Cors.new(proc {}) do |cfg|
cfg.allow do |allow|
allow.origins 'localhost:3000', '127.0.0.1:3000' do |source, env|
source == 'http://10.10.10.10:3000' &&
env['USER_AGENT'] == 'test-agent'
end
allow.resource '/get-only', methods: :get
allow.resource '/', headers: :any
end
end
resources = cors.send :all_resources
_(resources.length).must_equal 1
_(resources.first.allow_origin?('http://localhost:3000')).must_equal true
_(resources.first.allow_origin?('http://10.10.10.10:3000', { 'USER_AGENT' => 'test-agent' })).must_equal true
_(resources.first.allow_origin?('http://10.10.10.10:3001', { 'USER_AGENT' => 'test-agent' })).wont_equal true
_(resources.first.allow_origin?('http://10.10.10.10:3000', { 'USER_AGENT' => 'other-agent' })).wont_equal true
end
it 'should support implicit config object dsl mode' do
cors = Rack::Cors.new(proc {}) do
allow do
origins 'localhost:3000', '127.0.0.1:3000' do |source, env|
source == 'http://10.10.10.10:3000' &&
env['USER_AGENT'] == 'test-agent'
end
resource '/get-only', methods: :get
resource '/', headers: :any
end
end
resources = cors.send :all_resources
_(resources.length).must_equal 1
_(resources.first.allow_origin?('http://localhost:3000')).must_equal true
_(resources.first.allow_origin?('http://10.10.10.10:3000', { 'USER_AGENT' => 'test-agent' })).must_equal true
_(resources.first.allow_origin?('http://10.10.10.10:3001', { 'USER_AGENT' => 'test-agent' })).wont_equal true
_(resources.first.allow_origin?('http://10.10.10.10:3000', { 'USER_AGENT' => 'other-agent' })).wont_equal true
end
it 'should support "file://" origin' do
cors = Rack::Cors.new(proc {}) do
allow do
origins 'file://'
resource '/', headers: :any
end
end
resources = cors.send :all_resources
_(resources.first.allow_origin?('file://')).must_equal true
end
it 'should default credentials option to false' do
cors = Rack::Cors.new(proc {}) do
allow do
origins 'example.net'
resource '/', headers: :any
end
end
resources = cors.send :all_resources
_(resources.first.resources.first.credentials).must_equal false
end
end
rack-cors-2.0.1/test/unit/non_http.ru 0000644 0000041 0000041 00000000220 14415577125 017572 0 ustar www-data www-data # frozen_string_literal: true
require 'rack/cors'
use Rack::Cors do
allow do
origins 'com.company.app'
resource '/public'
end
end
rack-cors-2.0.1/test/unit/test.ru 0000644 0000041 0000041 00000003427 14415577125 016734 0 ustar www-data www-data # frozen_string_literal: true
require 'rack/cors'
# use Rack::Cors, :debug => true, :logger => ::Logger.new(STDOUT) do
use Rack::Lint
use Rack::Cors do
allow do
origins 'localhost:3000',
'127.0.0.1:3000',
%r{http://192\.168\.0\.\d{1,3}(:\d+)?},
'file://',
%r{http://(.*?)\.example\.com},
'custom-protocol://abcdefg'
resource '/get-only', methods: :get
resource '/', headers: :any, methods: :any
resource '/options', methods: :options
resource '/single_header', headers: 'x-domain-token'
resource '/two_headers', headers: %w[x-domain-token x-requested-with]
resource '/expose_single_header', expose: 'expose-test'
resource '/expose_multiple_headers', expose: %w[expose-test-1 expose-test-2]
resource '/conditional', methods: :get, if: proc { |env| !!env['HTTP_X_OK'] }
resource '/vary_test', methods: :get, vary: %w[Origin Host]
resource '/patch_test', methods: :patch
resource '/wildcard/*', methods: :any
# resource '/file/at/*',
# :methods => [:get, :post, :put, :delete],
# :headers => :any,
# :max_age => 0
end
allow do
origins do |source, _env|
source.end_with?('10.10.10.10:3000')
end
resource '/proc-origin'
end
allow do
origins ->(source, _env) { source.end_with?('10.10.10.10:3000') }
resource '/lambda-origin'
end
allow do
origins '*'
resource '/public'
resource '/public/*'
resource '/public_without_credentials', credentials: false
end
allow do
origins 'mucho-grande.com'
resource '/multi-allow-config', max_age: 600
end
allow do
origins '*'
resource '/multi-allow-config', max_age: 300, credentials: false
end
allow do
origins ''
resource '/blank-origin'
end
end
rack-cors-2.0.1/test/unit/insecure.ru 0000644 0000041 0000041 00000000225 14415577125 017563 0 ustar www-data www-data # frozen_string_literal: true
require 'rack/cors'
use Rack::Cors do
allow do
origins '*'
resource '/public', credentials: true
end
end
rack-cors-2.0.1/README.md 0000644 0000041 0000041 00000017627 14415577125 014735 0 ustar www-data www-data # Rack CORS Middleware [](https://github.com/cyu/rack-cors/actions)
`Rack::Cors` provides support for Cross-Origin Resource Sharing (CORS) for Rack compatible web applications.
The [CORS spec](http://www.w3.org/TR/cors/) allows web applications to make cross domain AJAX calls without using workarounds such as JSONP. See [further explanations on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
## Installation
Install the gem:
`gem install rack-cors`
Or in your Gemfile:
```ruby
gem 'rack-cors'
```
## Configuration
### Rails Configuration
For Rails, you'll need to add this middleware on application startup. A practical way to do this is with an initializer file. For example, the following will allow GET, POST, PATCH, or PUT requests from any origin on any resource:
```ruby
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :patch, :put]
end
end
```
NOTE: If you create application with `--api` option, configuration automatically generate in `config/initializers/cors.rb`.
We use `insert_before` to make sure `Rack::Cors` runs at the beginning of the stack to make sure it isn't interfered with by other middleware (see `Rack::Cache` note in **Common Gotchas** section). Basic setup examples for Rails 5 & Rails 6 can be found in the examples/ directory.
See The [Rails Guide to Rack](http://guides.rubyonrails.org/rails_on_rack.html) for more details on rack middlewares or watch the [railscast](http://railscasts.com/episodes/151-rack-middleware).
*Note about Rails 6*: Rails 6 has support for blocking requests from unknown hosts, so origin domains will need to be added there as well.
```ruby
Rails.application.config.hosts << "product.com"
```
Read more about it here in the [Rails Guides](https://guides.rubyonrails.org/configuring.html#configuring-middleware)
### Rack Configuration
NOTE: If you're running Rails, adding `config/initializers/cors.rb` should be enough. There is no need to update `config.ru` as well.
In `config.ru`, configure `Rack::Cors` by passing a block to the `use` command:
```ruby
use Rack::Cors do
allow do
origins 'localhost:3000', '127.0.0.1:3000',
/\Ahttp:\/\/192\.168\.0\.\d{1,3}(:\d+)?\z/
# regular expressions can be used here
resource '/file/list_all/', :headers => 'x-domain-token'
resource '/file/at/*',
methods: [:get, :post, :delete, :put, :patch, :options, :head],
headers: 'x-domain-token',
expose: ['Some-Custom-Response-Header'],
max_age: 600
# headers to expose
end
allow do
origins '*'
resource '/public/*', headers: :any, methods: :get
# Only allow a request for a specific host
resource '/api/v1/*',
headers: :any,
methods: :get,
if: proc { |env| env['HTTP_HOST'] == 'api.example.com' }
end
end
```
### Configuration Reference
#### Middleware Options
* **debug** (boolean): Enables debug logging and `X-Rack-CORS` HTTP headers for debugging.
* **logger** (Object or Proc): Specify the logger to log to. If a proc is provided, it will be called when a logger is needed. This is helpful in cases where the logger is initialized after `Rack::Cors` is initially configured, like `Rails.logger`.
#### Origin
Origins can be specified as a string, a regular expression, or as '\*' to allow all origins.
**\*SECURITY NOTE:** Be careful when using regular expressions to not accidentally be too inclusive. For example, the expression `/https:\/\/example\.com/` will match the domain *example.com.randomdomainname.co.uk*. It is recommended that any regular expression be enclosed with start & end string anchors, like `\Ahttps:\/\/example\.com\z`.
Additionally, origins can be specified dynamically via a block of the following form:
```ruby
origins { |source, env| true || false }
```
A Resource path can be specified as exact string match (`/path/to/file.txt`) or with a '\*' wildcard (`/all/files/in/*`). A resource can take the following options:
* **methods** (string or array or `:any`): The HTTP methods allowed for the resource.
* **headers** (string or array or `:any`): The HTTP headers that will be allowed in the CORS resource request. Use `:any` to allow for any headers in the actual request.
* **expose** (string or array): The HTTP headers in the resource response can be exposed to the client.
* **credentials** (boolean, default: `false`): Sets the `Access-Control-Allow-Credentials` response header. **Note:** If a wildcard (`*`) origin is specified, this option cannot be set to `true`. Read this [security article](http://web-in-security.blogspot.de/2017/07/cors-misconfigurations-on-large-scale.html) for more information.
* **max_age** (number): Sets the `Access-Control-Max-Age` response header.
* **if** (Proc): If the result of the proc is true, will process the request as a valid CORS request.
* **vary** (string or array): A list of HTTP headers to add to the 'Vary' header.
## Common Gotchas
### Origin Matching
When specifying an origin, make sure that it does not have a trailing slash.
### Testing Postman and/or cURL
* Make sure you're passing in an `Origin:` header. That header is required to trigger a CORS response. Here's [a good SO post](https://stackoverflow.com/questions/12173990/how-can-you-debug-a-cors-request-with-curl) about using cURL for testing CORS.
* Make sure your origin does not have a trailing slash.
### Positioning in the Middleware Stack
Positioning of `Rack::Cors` in the middleware stack is very important. In the Rails example above we put it above all other middleware which, in our experience, provides the most consistent results.
Here are some scenarios where incorrect positioning have created issues:
* **Serving static files.** Insert before `ActionDispatch::Static` so that static files are served with the proper CORS headers. **NOTE:** this might not work in production as static files are usually served from the web server (Nginx, Apache) and not the Rails container.
* **Caching in the middleware.** Insert before `Rack::Cache` so that the proper CORS headers are written and not cached ones.
* **Authentication via Warden** Warden will return immediately if a resource that requires authentication is accessed without authentication. If `Warden::Manager`is in the stack before `Rack::Cors`, it will return without the correct CORS headers being applied, resulting in a failed CORS request.
You can run the following command to see what the middleware stack looks like:
```bash
bundle exec rake middleware
```
Note that the middleware stack is different in production. For example, the `ActionDispatch::Static` middleware will not be part of the stack if `config.serve_static_assets = false`. You can run this to see what your middleware stack looks like in production:
```bash
RAILS_ENV=production bundle exec rake middleware
```
### Serving static files
If you trying to serve CORS headers on static assets (like CSS, JS, Font files), keep in mind that static files are usually served directly from web servers and never runs through the Rails container (including the middleware stack where `Rack::Cors` resides).
In Heroku, you can serve static assets through the Rails container by setting `config.serve_static_assets = true` in `production.rb`.
### Custom Protocols (chrome-extension://, ionic://, etc.)
Prior to 2.0.0, `http://`, `https://`, and `file://` are the only protocols supported in the `origins` list. If you wish to specify an origin that
has a custom protocol (`chrome-extension://`, `ionic://`, etc.) simply exclude the protocol. [See issue.](https://github.com/cyu/rack-cors/issues/100)
For example, instead of specifying `chrome-extension://aomjjhallfgjeglblehebfpbcfeobpga` specify `aomjjhallfgjeglblehebfpbcfeobpga` in `origins`.
As of 2.0.0 (currently in RC1), you can specify origins with a custom protocol.
rack-cors-2.0.1/CHANGELOG.md 0000644 0000041 0000041 00000005660 14415577125 015261 0 ustar www-data www-data # Change Log
All notable changes to this project will be documented in this file.
## 2.0.1 - 2023-02-17
### Changed
- Use Rack::Utils::HeaderHash when Rack 2.x is detected
## 2.0.0 - 2023-02-14
### Changed
- Refactored codebase
- Support declaring custom protocols in origin
- Lowercased header names as defined by Rack spec
- Fix issue with duplicate headers because of header name case
## 1.1.1 - 2019-12-29
### Changed
- Allow //* to match // and / paths
## 1.1.0 - 2019-11-19
### Changed
- Use Rack::Utils.escape_path instead of Rack::Utils.escape
- Require Rack 2.0 for escape_path method
- Don't try to clean path if invalid.
- Return 400 (Bad Request) on preflights with invalid path
## 1.0.6 - 2019-11-14
### Changed
- Use Rack::Utils.escape to make compat with Rack 1.6.0
## 1.0.5 - 2019-11-14
### Changed
- Update Gem spec to require rack >= 1.6.0
## 1.0.4 - 2019-11-13
### Security
- Escape and resolve path before evaluating resource rules (thanks to Colby Morgan)
## 1.0.3 - 2019-03-24
### Changed
- Don't send 'Content-Type' header with pre-flight requests
- Allow ruby array for vary header config
## 1.0.2 - 2017-10-22
### Fixed
- Automatically allow simple headers when headers are set
## 1.0.1 - 2017-07-18
### Fixed
- Allow lambda origin configuration
## 1.0.0 - 2017-07-15
### Security
- Don't implicitly accept 'null' origins when 'file://' is specified
(https://github.com/cyu/rack-cors/pull/134)
- Ignore '' origins (https://github.com/cyu/rack-cors/issues/139)
- Default credentials option on resources to false
(https://github.com/cyu/rack-cors/issues/95)
- Don't allow credentials option to be true if '*' is specified is origin
(https://github.com/cyu/rack-cors/pull/142)
- Don't reflect Origin header when '*' is specified as origin
(https://github.com/cyu/rack-cors/pull/142)
### Fixed
- Don't respond immediately on non-matching preflight requests instead of
sending them through the app (https://github.com/cyu/rack-cors/pull/106)
## 0.4.1 - 2017-02-01
### Fixed
- Return miss result in X-Rack-CORS instead of incorrectly returning
preflight-hit
## 0.4.0 - 2015-04-15
### Changed
- Don't set HTTP_ORIGIN with HTTP_X_ORIGIN if nil
### Added
- Calculate vary headers for non-CORS resources
- Support custom vary headers for resource
- Support :if option for resource
- Support :any as a possible value for :methods option
### Fixed
- Don't symbolize incoming HTTP request methods
## 0.3.1 - 2014-12-27
### Changed
- Changed the env key to rack.cors to avoid Rack::Lint warnings
## 0.3.0 - 2014-10-19
### Added
- Added support for defining a logger with a Proc
- Return a X-Rack-CORS header when in debug mode detailing how Rack::Cors
processed a request
- Added support for non HTTP/HTTPS origins when just a domain is specified
### Changed
- Changed the log level of the fallback logger to DEBUG
- Print warning when attempting to use :any as an allowed method
- Treat incoming `Origin: null` headers as file://
rack-cors-2.0.1/.rubocop.yml 0000644 0000041 0000041 00000001057 14415577125 015716 0 ustar www-data www-data ---
AllCops:
Exclude:
- "examples/**/*"
- "vendor/**/*"
# Disables
Layout/LineLength:
Enabled: false
Style/Documentation:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/BlockLength:
Enabled: false
Style/HashEachMethods:
Enabled: false
Style/HashTransformKeys:
Enabled: false
Style/HashTransformValues:
Enabled: false
Style/DoubleNegation:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Metrics/AbcSize:
Enabled: false
rack-cors-2.0.1/Rakefile 0000644 0000041 0000041 00000000756 14415577125 015116 0 ustar www-data www-data # frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
task default: :test
require 'rdoc/task'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ''
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "rack-cors #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
rack-cors-2.0.1/lib/ 0000755 0000041 0000041 00000000000 14415577125 014207 5 ustar www-data www-data rack-cors-2.0.1/lib/rack/ 0000755 0000041 0000041 00000000000 14415577125 015127 5 ustar www-data www-data rack-cors-2.0.1/lib/rack/cors.rb 0000644 0000041 0000041 00000013260 14415577125 016424 0 ustar www-data www-data # frozen_string_literal: true
require 'logger'
require_relative 'cors/resources'
require_relative 'cors/resource'
require_relative 'cors/result'
require_relative 'cors/version'
module Rack
class Cors
HTTP_ORIGIN = 'HTTP_ORIGIN'
HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'
HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'
HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
PATH_INFO = 'PATH_INFO'
REQUEST_METHOD = 'REQUEST_METHOD'
RACK_LOGGER = 'rack.logger'
RACK_CORS =
# retaining the old key for backwards compatibility
ENV_KEY = 'rack.cors'
OPTIONS = 'OPTIONS'
DEFAULT_VARY_HEADERS = ['Origin'].freeze
def initialize(app, opts = {}, &block)
@app = app
@debug_mode = !!opts[:debug]
@logger = @logger_proc = nil
logger = opts[:logger]
if logger
if logger.respond_to? :call
@logger_proc = opts[:logger]
else
@logger = logger
end
end
return unless block_given?
if block.arity == 1
block.call(self)
else
instance_eval(&block)
end
end
def debug?
@debug_mode
end
def allow(&block)
all_resources << (resources = Resources.new)
if block.arity == 1
block.call(resources)
else
resources.instance_eval(&block)
end
end
def call(env)
env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
path = evaluate_path(env)
add_headers = nil
if env[HTTP_ORIGIN]
debug(env) do
['Incoming Headers:',
" Origin: #{env[HTTP_ORIGIN]}",
" Path-Info: #{path}",
" Access-Control-Request-Method: #{env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]}",
" Access-Control-Request-Headers: #{env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]}"].join("\n")
end
if env[REQUEST_METHOD] == OPTIONS && env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
return [400, {}, []] unless Rack::Utils.valid_path?(path)
headers = process_preflight(env, path)
debug(env) do
"Preflight Headers:\n" +
headers.collect { |kv| " #{kv.join(': ')}" }.join("\n")
end
return [200, headers, []]
else
add_headers = process_cors(env, path)
end
else
Result.miss(env, Result::MISS_NO_ORIGIN)
end
# This call must be done BEFORE calling the app because for some reason
# env[PATH_INFO] gets changed after that and it won't match. (At least
# in rails 4.1.6)
vary_resource = resource_for_path(path)
status, headers, body = @app.call env
if add_headers
headers = add_headers.merge(headers)
debug(env) do
add_headers.each_pair do |key, value|
headers["x-rack-cors-original-#{key}"] = value if headers.key?(key)
end
end
end
# Vary header should ALWAYS mention Origin if there's ANY chance for the
# response to be different depending on the Origin header value.
# Better explained here: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
if vary_resource
vary = headers['vary']
cors_vary_headers = if vary_resource.vary_headers&.any?
vary_resource.vary_headers
else
DEFAULT_VARY_HEADERS
end
headers['vary'] = ((vary ? [vary].flatten.map { |v| v.split(/,\s*/) }.flatten : []) + cors_vary_headers).uniq.join(', ')
end
result = env[ENV_KEY]
result.append_header(headers) if debug? && result
[status, headers, body]
end
protected
def debug(env, message = nil, &block)
(@logger || select_logger(env)).debug(message, &block) if debug?
end
def select_logger(env)
@logger = if @logger_proc
logger_proc = @logger_proc
@logger_proc = nil
logger_proc.call
elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
Rails.logger
elsif env[RACK_LOGGER]
env[RACK_LOGGER]
else
::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
end
end
def evaluate_path(env)
path = env[PATH_INFO]
if path
path = Rack::Utils.unescape_path(path)
path = Rack::Utils.clean_path_info(path) if Rack::Utils.valid_path?(path)
end
path
end
def all_resources
@all_resources ||= []
end
def process_preflight(env, path)
result = Result.preflight(env)
resource, error = match_resource(path, env)
unless resource
result.miss(error)
return {}
end
resource.process_preflight(env, result)
end
def process_cors(env, path)
resource, error = match_resource(path, env)
if resource
Result.hit(env)
cors = resource.to_headers(env)
cors
else
Result.miss(env, error)
nil
end
end
def resource_for_path(path_info)
all_resources.each do |r|
found = r.resource_for_path(path_info)
return found if found
end
nil
end
def match_resource(path, env)
origin = env[HTTP_ORIGIN]
origin_matched = false
all_resources.each do |r|
next unless r.allow_origin?(origin, env)
origin_matched = true
found = r.match_resource(path, env)
return [found, nil] if found
end
[nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
end
end
end
rack-cors-2.0.1/lib/rack/cors/ 0000755 0000041 0000041 00000000000 14415577125 016075 5 ustar www-data www-data rack-cors-2.0.1/lib/rack/cors/resources.rb 0000644 0000041 0000041 00000002633 14415577125 020440 0 ustar www-data www-data # frozen_string_literal: true
require_relative 'resources/cors_misconfiguration_error'
module Rack
class Cors
class Resources
attr_reader :resources
def initialize
@origins = []
@resources = []
@public_resources = false
end
def origins(*args, &blk)
@origins = args.flatten.reject { |s| s == '' }.map do |n|
case n
when Proc, Regexp, %r{^[a-z][a-z0-9.+-]*://}
n
when '*'
@public_resources = true
n
else
Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
end
end.flatten
@origins.push(blk) if blk
end
def resource(path, opts = {})
@resources << Resource.new(public_resources?, path, opts)
end
def public_resources?
@public_resources
end
def allow_origin?(source, env = {})
return true if public_resources?
!!@origins.detect do |origin|
if origin.is_a?(Proc)
origin.call(source, env)
elsif origin.is_a?(Regexp)
source =~ origin
else
source == origin
end
end
end
def match_resource(path, env)
@resources.detect { |r| r.match?(path, env) }
end
def resource_for_path(path)
@resources.detect { |r| r.matches_path?(path) }
end
end
end
end
rack-cors-2.0.1/lib/rack/cors/version.rb 0000644 0000041 0000041 00000000130 14415577125 020101 0 ustar www-data www-data # frozen_string_literal: true
module Rack
class Cors
VERSION = '2.0.1'
end
end
rack-cors-2.0.1/lib/rack/cors/resources/ 0000755 0000041 0000041 00000000000 14415577125 020107 5 ustar www-data www-data rack-cors-2.0.1/lib/rack/cors/resources/cors_misconfiguration_error.rb 0000644 0000041 0000041 00000000561 14415577125 026255 0 ustar www-data www-data # frozen_string_literal: true
module Rack
class Cors
class Resource
class CorsMisconfigurationError < StandardError
def message
'Allowing credentials for wildcard origins is insecure.' \
" Please specify more restrictive origins or set 'credentials' to false in your CORS configuration."
end
end
end
end
end
rack-cors-2.0.1/lib/rack/cors/result.rb 0000644 0000041 0000041 00000002620 14415577125 017740 0 ustar www-data www-data # frozen_string_literal: true
module Rack
class Cors
class Result
HEADER_KEY = 'x-rack-cors'
MISS_NO_ORIGIN = 'no-origin'
MISS_NO_PATH = 'no-path'
MISS_NO_METHOD = 'no-method'
MISS_DENY_METHOD = 'deny-method'
MISS_DENY_HEADER = 'deny-header'
attr_accessor :preflight, :hit, :miss_reason
def hit?
!!hit
end
def preflight?
!!preflight
end
def miss(reason)
self.hit = false
self.miss_reason = reason
end
def self.hit(env)
r = Result.new
r.preflight = false
r.hit = true
env[Rack::Cors::ENV_KEY] = r
end
def self.miss(env, reason)
r = Result.new
r.preflight = false
r.hit = false
r.miss_reason = reason
env[Rack::Cors::ENV_KEY] = r
end
def self.preflight(env)
r = Result.new
r.preflight = true
env[Rack::Cors::ENV_KEY] = r
end
def append_header(headers)
headers[HEADER_KEY] = if hit?
preflight? ? 'preflight-hit' : 'hit'
else
[
(preflight? ? 'preflight-miss' : 'miss'),
miss_reason
].join('; ')
end
end
end
end
end
rack-cors-2.0.1/lib/rack/cors/resource.rb 0000644 0000041 0000041 00000010772 14415577125 020260 0 ustar www-data www-data # frozen_string_literal: true
module Rack
class Cors
class Resource
# All CORS routes need to accept CORS simple headers at all times
# {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers}
CORS_SIMPLE_HEADERS = %w[accept accept-language content-language content-type].freeze
attr_accessor :path, :methods, :headers, :expose, :max_age, :credentials, :pattern, :if_proc, :vary_headers
def initialize(public_resource, path, opts = {})
raise CorsMisconfigurationError if public_resource && opts[:credentials] == true
self.path = path
self.credentials = public_resource ? false : (opts[:credentials] == true)
self.max_age = opts[:max_age] || 7200
self.pattern = compile(path)
self.if_proc = opts[:if]
self.vary_headers = opts[:vary] && [opts[:vary]].flatten
@public_resource = public_resource
self.headers = case opts[:headers]
when :any then :any
when nil then nil
else
[opts[:headers]].flatten.collect(&:downcase)
end
self.methods = case opts[:methods]
when :any then %i[get head post put patch delete options]
else
ensure_enum(opts[:methods]) || [:get]
end.map(&:to_s)
self.expose = opts[:expose] ? [opts[:expose]].flatten : nil
end
def matches_path?(path)
pattern =~ path
end
def match?(path, env)
matches_path?(path) && (if_proc.nil? || if_proc.call(env))
end
def process_preflight(env, result)
headers = {}
request_method = env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_METHOD]
result.miss(Result::MISS_NO_METHOD) && (return headers) if request_method.nil?
result.miss(Result::MISS_DENY_METHOD) && (return headers) unless methods.include?(request_method.downcase)
request_headers = env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
result.miss(Result::MISS_DENY_HEADER) && (return headers) if request_headers && !allow_headers?(request_headers)
result.hit = true
headers.merge(to_preflight_headers(env))
end
def to_headers(env)
h = {
'access-control-allow-origin' => origin_for_response_header(env[Rack::Cors::HTTP_ORIGIN]),
'access-control-allow-methods' => methods.collect { |m| m.to_s.upcase }.join(', '),
'access-control-expose-headers' => expose.nil? ? '' : expose.join(', '),
'access-control-max-age' => max_age.to_s
}
h['access-control-allow-credentials'] = 'true' if credentials
header_proc.call(h)
end
protected
def public_resource?
@public_resource
end
def origin_for_response_header(origin)
return '*' if public_resource?
origin
end
def to_preflight_headers(env)
h = to_headers(env)
h.merge!('access-control-allow-headers' => env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_HEADERS]) if env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
h
end
def allow_headers?(request_headers)
headers = self.headers || []
return true if headers == :any
request_headers = request_headers.split(/,\s*/) if request_headers.is_a?(String)
request_headers.all? do |header|
header = header.downcase
CORS_SIMPLE_HEADERS.include?(header) || headers.include?(header)
end
end
def ensure_enum(var)
return nil if var.nil?
[var].flatten
end
def compile(path)
if path.respond_to? :to_str
special_chars = %w[. + ( )]
pattern =
path.to_str.gsub(%r{((:\w+)|/\*|[\*#{special_chars.join}])}) do |match|
case match
when '/*'
'\\/?(.*?)'
when '*'
'(.*?)'
when *special_chars
Regexp.escape(match)
else
'([^/?]+)'
end
end
/^#{pattern}$/
elsif path.respond_to? :match
path
else
raise TypeError, path
end
end
def header_proc
@header_proc ||= begin
if defined?(Rack::Headers)
->(h) { h }
else
->(h) { Rack::Utils::HeaderHash.new(h) }
end
end
end
end
end
end
rack-cors-2.0.1/Gemfile 0000644 0000041 0000041 00000000233 14415577125 014732 0 ustar www-data www-data # frozen_string_literal: true
source 'https://rubygems.org'
# Specify your gem's dependencies in rack-cors.gemspec
gemspec
gem 'pry-byebug', '~> 3.6.0'
rack-cors-2.0.1/rack-cors.gemspec 0000644 0000041 0000041 00000002601 14415577125 016671 0 ustar www-data www-data # frozen_string_literal: true
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'rack/cors/version'
Gem::Specification.new do |spec|
spec.name = 'rack-cors'
spec.version = Rack::Cors::VERSION
spec.authors = ['Calvin Yu']
spec.email = ['me@sourcebender.com']
spec.description = 'Middleware that will make Rack-based apps CORS compatible. Fork the project here: https://github.com/cyu/rack-cors'
spec.summary = 'Middleware for enabling Cross-Origin Resource Sharing in Rack apps'
spec.homepage = 'https://github.com/cyu/rack-cors'
spec.license = 'MIT'
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR).reject { |f| (f == '.gitignore') || f =~ /^examples/ }
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']
spec.add_dependency 'rack', '>= 2.0.0'
spec.add_development_dependency 'bundler', '>= 1.16.0', '< 3'
spec.add_development_dependency 'minitest', '~> 5.11.0'
spec.add_development_dependency 'mocha', '~> 1.6.0'
spec.add_development_dependency 'pry', '~> 0.12'
spec.add_development_dependency 'rack-test', '>= 1.1.0'
spec.add_development_dependency 'rake', '~> 12.3.0'
spec.add_development_dependency 'rubocop', '~> 0.80.1'
end
rack-cors-2.0.1/.github/ 0000755 0000041 0000041 00000000000 14415577125 015001 5 ustar www-data www-data rack-cors-2.0.1/.github/workflows/ 0000755 0000041 0000041 00000000000 14415577125 017036 5 ustar www-data www-data rack-cors-2.0.1/.github/workflows/ci.yaml 0000644 0000041 0000041 00000001350 14415577125 020314 0 ustar www-data www-data name: ci
on:
- push
- pull_request
jobs:
test:
strategy:
fail-fast: false
matrix:
ruby:
- "2.3"
- "2.4"
- "2.5"
- "2.6"
- "2.7"
- "3.0"
- "3.1"
- "3.2"
- truffleruby-head
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rake test
rubocop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.1
bundler-cache: true
- run: bundle exec rubocop
rack-cors-2.0.1/LICENSE.txt 0000644 0000041 0000041 00000002052 14415577125 015263 0 ustar www-data www-data Copyright (c) 2013 Calvin Yu
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:
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.