<commit_before>define([
    '/api/config?cb=' + Math.random().toString(16).substring(2),
    '/common/messages.js',
    '/common/crypto.js',
    '/_socket/realtime-input.js',
    '/common/hyperjson.js',
    '/common/hyperscript.js',
    '/_socket/toolbar.js',
    '/common/cursor.js',
    '/common/json-ot.js',
    '/_socket/typingTest.js',
    '/bower_components/diff-dom/diffDOM.js',
    '/bower_components/jquery/dist/jquery.min.js',
    '/customize/pad.js'
], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT, TypingTest) {
    var $ = window.jQuery;
    var ifrw = $('#pad-iframe')[0].contentWindow;
    var Ckeditor; // to be initialized later...
    var DiffDom = window.diffDOM;

    var hjsonToDom = function (H) {
        return Hyperjson.callOn(H, Hyperscript);
    };

    var userName = Crypto.rand64(8),
        toolbar;

    var module = {};

    var isNotMagicLine = function (el) {
        // factor as:
        // return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
        var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false');
        if (filter) {
            console.log("[hyperjson.serializer] prevented an element" +
                "from being serialized:", el);
            return false;
        }
        return true;
    };

    var andThen = function (Ckeditor) {
        $(window).on('hashchange', function() {
            window.location.reload();
        });
        if (window.location.href.indexOf('#') === -1) {
            window.location.href = window.location.href + '#' + Crypto.genKey();
            return;
        }

        var fixThings = false;
        var key = Crypto.parseKey(window.location.hash.substring(1));
        var editor = window.editor = Ckeditor.replace('editor1', {
            // https://dev.ckeditor.com/ticket/10907
            needsBrFiller: fixThings,
            needsNbspFiller: fixThings,
            removeButtons: 'Source,Maximize',
            // magicline plugin inserts html crap into the document which is not part of the
            // document itself and causes problems when it's sent across the wire and reflected back
            // but we filter it now, so that's ok.
            removePlugins: 'resize'
        });

        editor.on('instanceReady', function (Ckeditor) {
            editor.execCommand('maximize');
            var documentBody = ifrw.$('iframe')[0].contentDocument.body;

            documentBody.innerHTML = Messages.initialState;

            var inner = window.inner = documentBody;
            var cursor = window.cursor = Cursor(inner);

            var setEditable = function (bool) {
                // careful about putting attributes onto the DOM
                // they get put into the chain, and you can have trouble
                // getting rid of them later

                //inner.style.backgroundColor = bool? 'white': 'grey';
                inner.setAttribute('contenteditable', bool);
            };

            // don't let the user edit until the pad is ready
            setEditable(false);

            var diffOptions = {
                preDiffApply: function (info) {
                    /* DiffDOM will filter out magicline plugin elements
                        in practice this will make it impossible to use it
                        while someone else is typing, which could be annoying.

                        we should check when such an element is going to be
                        removed, and prevent that from happening. */
                    if (info.node && info.node.tagName === 'SPAN' &&
                        info.node.contentEditable === "true") {
                        // it seems to be a magicline plugin element...
                        if (info.diff.action === 'removeElement') {
                            // and you're about to remove it...
                            // this probably isn't what you want

                            /*
                                I have never seen this in the console, but the
                                magic line is still getting removed on remote
                                edits. This suggests that it's getting removed
                                by something other than diffDom.
                            */
                            console.log("preventing removal of the magic line!");

                            // return true to prevent diff application
                            return true;
                        }
                    }

                    // no use trying to recover the cursor if it doesn't exist
                    if (!cursor.exists()) { return; }

                    /*  frame is either 0, 1, 2, or 3, depending on which
                        cursor frames were affected: none, first, last, or both
                    */
                    var frame = info.frame = cursor.inNode(info.node);

                    if (!frame) { return; }

                    if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
                        var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);

                        if (frame & 1) {
                            // push cursor start if necessary
                            if (pushes.commonStart < cursor.Range.start.offset) {
                                cursor.Range.start.offset += pushes.delta;
                            }
                        }
                        if (frame & 2) {
                            // push cursor end if necessary
                            if (pushes.commonStart < cursor.Range.end.offset) {
                                cursor.Range.end.offset += pushes.delta;
                            }
                        }
                    }
                },
                postDiffApply: function (info) {
                    if (info.frame) {
                        if (info.node) {
                            if (info.frame & 1) { cursor.fixStart(info.node); }
                            if (info.frame & 2) { cursor.fixEnd(info.node); }
                        } else { console.error("info.node did not exist"); }

                        var sel = cursor.makeSelection();
                        var range = cursor.makeRange();

                        cursor.fixSelection(sel, range);
                    }
                }
            };

            var now = function () { return new Date().getTime(); };

            var realtimeOptions = {
                // configuration :D
                doc: inner,

                // provide initialstate...
                initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)),

                // really basic operational transform
                // reject patch if it results in invalid JSON
                transformFunction : JsonOT.validate,

                websocketURL: Config.websocketURL,

                // username
                userName: userName,

                // communication channel name
                channel: key.channel,

                // encryption key
                cryptKey: key.cryptKey
            };

            var DD = new DiffDom(diffOptions);

            // apply patches, and try not to lose the cursor in the process!
            var applyHjson = function (shjson) {
                var userDocStateDom = hjsonToDom(JSON.parse(shjson));
                userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
                var patch = (DD).diff(inner, userDocStateDom);
                (DD).apply(inner, patch);
            };

            var initializing = true;

            var onRemote = realtimeOptions.onRemote = function (info) {
                if (initializing) { return; }

                var shjson = info.realtime.getUserDoc();

                // remember where the cursor is
                cursor.update();

                // build a dom from HJSON, diff, and patch the editor
                applyHjson(shjson);

                var shjson2 = JSON.stringify(Hyperjson.fromDOM(inner));
                if (shjson2 !== shjson) {
                    module.realtimeInput.patchText(shjson2);
                }
            };

            var onInit = realtimeOptions.onInit = function (info) {
                var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
                toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime);
                /* TODO handle disconnects and such*/
            };

            var onReady = realtimeOptions.onReady = function (info) {
                console.log("Unlocking editor");
                initializing = false;
                setEditable(true);

                var shjson = info.realtime.getUserDoc();

                applyHjson(shjson);
            };

            var onAbort = realtimeOptions.onAbort = function (info) {
                console.log("Aborting the session!");
                // stop the user from continuing to edit
                // by setting the editable to false
                setEditable(false);
                toolbar.failed();
            };

            var rti = window.CRYPTPAD_REALTIME = module.realtimeInput =
                realtimeInput.start(realtimeOptions);

            var brFilter = function (hj) {
                if (hj[1].type === '_moz') { hj[1].type = undefined; }
                return hj;
            };

            /*
                It's incredibly important that you assign 'rti.onLocal'
                It's used inside of realtimeInput to make sure that all changes
                make it into chainpad.

                It's being assigned this way because it can't be passed in, and
                and can't be easily returned from realtime input without making
                the code less extensible.
            */
            var propogate = rti.onLocal = function () {
                var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine, brFilter));
                if (!rti.patchText(shjson)) { return; }
                //rti.onEvent(shjson);
            };

            /* hitting enter makes a new line, but places the cursor inside
                of the
 instead of the
. This makes it such that you
                cannot type until you click, which is rather unnacceptable.
                If the cursor is ever inside such a
, you probably want
                to push it out to the parent element, which ought to be a
                paragraph tag. This needs to be done on keydown, otherwise
                the first such keypress will not be inserted into the P. */
            inner.addEventListener('keydown', cursor.brFix);

            var easyTest = window.easyTest = function () {
                cursor.update();
                var start = cursor.Range.start;
                var test = TypingTest.testInput(inner, start.el, start.offset, propogate);
                propogate();
                return test;
            };

            editor.on('change', propogate);
        });
    };

    var interval = 100;
    var first = function () {
        Ckeditor = ifrw.CKEDITOR;
        if (Ckeditor) {
            andThen(Ckeditor);
        } else {
            console.log("Ckeditor was not defined. Trying again in %sms",interval);
            setTimeout(first, interval);
        }
    };

    $(first);
});
<commit_msg>Debugging concurrent typing:<commit_after>define([
    '/api/config?cb=' + Math.random().toString(16).substring(2),
    '/common/messages.js',
    '/common/crypto.js',
    '/_socket/realtime-input.js',
    '/common/hyperjson.js',
    '/common/hyperscript.js',
    '/_socket/toolbar.js',
    '/common/cursor.js',
    '/common/json-ot.js',
    '/_socket/typingTest.js',
    '/bower_components/diff-dom/diffDOM.js',
    '/bower_components/jquery/dist/jquery.min.js',
    '/customize/pad.js'
], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT, TypingTest) {
    var $ = window.jQuery;
    var ifrw = $('#pad-iframe')[0].contentWindow;
    var Ckeditor; // to be initialized later...
    var DiffDom = window.diffDOM;

    window.Hyperjson = Hyperjson;

    var hjsonToDom = function (H) {
        return Hyperjson.callOn(H, Hyperscript);
    };

    var userName = Crypto.rand64(8),
        toolbar;

    var module = window.REALTIME_MODULE = {
        localChangeInProgress: 0
    };

    var isNotMagicLine = function (el) {
        // factor as:
        // return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
        var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false');
        if (filter) {
            console.log("[hyperjson.serializer] prevented an element" +
                "from being serialized:", el);
            return false;
        }
        return true;
    };

    var andThen = function (Ckeditor) {
        $(window).on('hashchange', function() {
            window.location.reload();
        });
        if (window.location.href.indexOf('#') === -1) {
            window.location.href = window.location.href + '#' + Crypto.genKey();
            return;
        }

        var fixThings = false;
        var key = Crypto.parseKey(window.location.hash.substring(1));
        var editor = window.editor = Ckeditor.replace('editor1', {
            // https://dev.ckeditor.com/ticket/10907
            needsBrFiller: fixThings,
            needsNbspFiller: fixThings,
            removeButtons: 'Source,Maximize',
            // magicline plugin inserts html crap into the document which is not part of the
            // document itself and causes problems when it's sent across the wire and reflected back
            // but we filter it now, so that's ok.
            removePlugins: 'resize'
        });

        editor.on('instanceReady', function (Ckeditor) {
            editor.execCommand('maximize');
            var documentBody = ifrw.$('iframe')[0].contentDocument.body;

            documentBody.innerHTML = Messages.initialState;

            var inner = window.inner = documentBody;
            var cursor = window.cursor = Cursor(inner);

            var setEditable = function (bool) {
                // careful about putting attributes onto the DOM
                // they get put into the chain, and you can have trouble
                // getting rid of them later

                //inner.style.backgroundColor = bool? 'white': 'grey';
                inner.setAttribute('contenteditable', bool);
            };

            // don't let the user edit until the pad is ready
            setEditable(false);

            var diffOptions = {
                preDiffApply: function (info) {
                    /* DiffDOM will filter out magicline plugin elements
                        in practice this will make it impossible to use it
                        while someone else is typing, which could be annoying.

                        we should check when such an element is going to be
                        removed, and prevent that from happening. */
                    if (info.node && info.node.tagName === 'SPAN' &&
                        info.node.contentEditable === "true") {
                        // it seems to be a magicline plugin element...
                        if (info.diff.action === 'removeElement') {
                            // and you're about to remove it...
                            // this probably isn't what you want

                            /*
                                I have never seen this in the console, but the
                                magic line is still getting removed on remote
                                edits. This suggests that it's getting removed
                                by something other than diffDom.
                            */
                            console.log("preventing removal of the magic line!");

                            // return true to prevent diff application
                            return true;
                        }
                    }

                    // no use trying to recover the cursor if it doesn't exist
                    if (!cursor.exists()) { return; }

                    /*  frame is either 0, 1, 2, or 3, depending on which
                        cursor frames were affected: none, first, last, or both
                    */
                    var frame = info.frame = cursor.inNode(info.node);

                    if (!frame) { return; }

                    if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
                        var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);

                        if (frame & 1) {
                            // push cursor start if necessary
                            if (pushes.commonStart < cursor.Range.start.offset) {
                                cursor.Range.start.offset += pushes.delta;
                            }
                        }
                        if (frame & 2) {
                            // push cursor end if necessary
                            if (pushes.commonStart < cursor.Range.end.offset) {
                                cursor.Range.end.offset += pushes.delta;
                            }
                        }
                    }
                },
                postDiffApply: function (info) {
                    if (info.frame) {
                        if (info.node) {
                            if (info.frame & 1) { cursor.fixStart(info.node); }
                            if (info.frame & 2) { cursor.fixEnd(info.node); }
                        } else { console.error("info.node did not exist"); }

                        var sel = cursor.makeSelection();
                        var range = cursor.makeRange();

                        cursor.fixSelection(sel, range);
                    }
                }
            };

            var now = function () { return new Date().getTime(); };

            var realtimeOptions = {
                // configuration :D
                doc: inner,

                // provide initialstate...
                initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)),

                // really basic operational transform
                // reject patch if it results in invalid JSON
                transformFunction : JsonOT.validate,

                websocketURL: Config.websocketURL,

                // username
                userName: userName,

                // communication channel name
                channel: key.channel,

                // encryption key
                cryptKey: key.cryptKey
            };

            var DD = new DiffDom(diffOptions);

            var localWorkInProgress = function (stage) {
                if (module.localChangeInProgress) {
                    console.error("Applied a change while a local patch was in progress");
                    alert("local work was interrupted at stage: " + stage);
                    //module.realtimeInput.onLocal();
                    return true;
                }
                return false;
            };

            // apply patches, and try not to lose the cursor in the process!
            var applyHjson = function (shjson) {

                localWorkInProgress(1); // check if this would interrupt local work

                var userDocStateDom = hjsonToDom(JSON.parse(shjson));
                localWorkInProgress(2); // check again
                userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
                localWorkInProgress(3); // check again
                var patch = (DD).diff(inner, userDocStateDom);
                localWorkInProgress(4); // check again
                (DD).apply(inner, patch);
                localWorkInProgress(5); // check again
            };

            var initializing = true;

            var onRemote = realtimeOptions.onRemote = function (info) {
                if (initializing) { return; }

                localWorkInProgress(0);

                var shjson = info.realtime.getUserDoc();

                // remember where the cursor is
                cursor.update();

                // build a dom from HJSON, diff, and patch the editor
                applyHjson(shjson);

                var shjson2 = JSON.stringify(Hyperjson.fromDOM(inner));
                if (shjson2 !== shjson) {
                    console.error("shjson2 !== shjson");
                    module.realtimeInput.patchText(shjson2);
                }
            };

            var onInit = realtimeOptions.onInit = function (info) {
                var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
                toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime);

                /* TODO handle disconnects and such*/
            };

            var onReady = realtimeOptions.onReady = function (info) {
                console.log("Unlocking editor");
                initializing = false;
                setEditable(true);

                var shjson = info.realtime.getUserDoc();

                applyHjson(shjson);
            };

            var onAbort = realtimeOptions.onAbort = function (info) {
                console.log("Aborting the session!");
                // stop the user from continuing to edit
                // by setting the editable to false
                setEditable(false);
                toolbar.failed();
            };

            var rti = module.realtimeInput = realtimeInput.start(realtimeOptions);

            /* catch `type="_moz"` before it goes over the wire */
            var brFilter = function (hj) {
                if (hj[1].type === '_moz') { hj[1].type = undefined; }
                return hj;
            };

            /*  It's incredibly important that you assign 'rti.onLocal'
                It's used inside of realtimeInput to make sure that all changes
                make it into chainpad.

                It's being assigned this way because it can't be passed in, and
                and can't be easily returned from realtime input without making
                the code less extensible.
            */
            var propogate = rti.onLocal = function () {
                /*  if the problem were a matter of external patches being
                    applied while a local patch were in progress, then we would
                    expect to be able to check and find
                    'module.localChangeInProgress' with a non-zero value while
                    we were applying a remote change.
                */
                module.localChangeInProgress += 1;
                var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine, brFilter));
                if (!rti.patchText(shjson)) {
                    module.localChangeInProgress -= 1;
                    return;
                }
                rti.onEvent(shjson);
                module.localChangeInProgress -= 1;
            };

            /* hitting enter makes a new line, but places the cursor inside
                of the
 instead of the
. This makes it such that you
                cannot type until you click, which is rather unnacceptable.
                If the cursor is ever inside such a
, you probably want
                to push it out to the parent element, which ought to be a
                paragraph tag. This needs to be done on keydown, otherwise
                the first such keypress will not be inserted into the P. */
            inner.addEventListener('keydown', cursor.brFix);

            var easyTest = window.easyTest = function () {
                cursor.update();
                var start = cursor.Range.start;
                var test = TypingTest.testInput(inner, start.el, start.offset, propogate);
                propogate();
                return test;
            };

            editor.on('change', propogate);
        });
    };

    var interval = 100;
    var first = function () {
        Ckeditor = ifrw.CKEDITOR;
        if (Ckeditor) {
            andThen(Ckeditor);
        } else {
            console.log("Ckeditor was not defined. Trying again in %sms",interval);
            setTimeout(first, interval);
        }
    };

    $(first);
});
<|endoftext|>


<commit_before>'use strict';

var grunt = require('grunt');
var http = require('http');
var https = require('https');
var http2 = require('http2');

function get(url, done) {
  var client = http;
  if (typeof url === 'object' && url.scheme === 'http2') {
    client = http2;
    delete url.scheme;
  }
  else if (typeof url === 'string' && url.toLowerCase().indexOf('https') === 0 ||
    typeof url === 'object' && url.port === 443 ||
    typeof url === 'object' && url.scheme === 'https') {
    client = https;
    delete url.scheme;
  }
  client.get(url, function(res) {
    var body = '';
    res.on('data', function(chunk) {
      body += chunk;
    }).on('end', function() {
      done(res, body);
    });
  });
}

exports.connect = {
  custom_base: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8000,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_port: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8001,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_http2: function(test) {
    if (!grunt.config.data.connect.http2) {
      test.done();
    }
    else {
      test.expect(3);
      get({
        scheme: 'http2',
        rejectUnauthorized: false,
        hostname: 'localhost',
        port: 8017,
        path: '/fixtures/hello.txt',
        headers: {
          accept: 'text/plain'
        }
      }, function(res, body) {
        test.equal(res.httpVersion, '2.0', 'should return HTTP/2 response');
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'should return static page');
        test.done();
      });
    }
  },
  custom_https: function(test) {
    test.expect(2);
    get({
      scheme: 'https',
      /* per http://nodejs.org/api/https.html#https_https_request_options_callback
         allow certs signed by an untrusted CA */
      rejectUnauthorized: false,
      hostname: 'localhost',
      port: 8002,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_https_certs: function(test) {
    test.expect(2);
    get({
      scheme: 'https',
      rejectUnauthorized: false,
      hostname: 'localhost',
      port: 8003,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_base_with_options: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8014,
      path: '/',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  multiple_base: function(test) {
    test.expect(4);
    get({
      hostname: 'localhost',
      port: 8004,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(res.headers['content-type'], 'text/plain; charset=UTF-8', 'should return plaintext content type');
      get('http://localhost:8004/connect-examples.md', function(res, body) {
        test.equal(res.headers['content-type'], 'text/x-markdown; charset=UTF-8', 'should return markdown content type');
        test.equal(res.statusCode, 200, 'should return 200');
        test.done();
      });
    });
  },
  multiple_base_with_options: function(test) {
    test.expect(5);
    get({
      hostname: 'localhost',
      port: 8015,
      path: '/',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.ok(res.headers['cache-control'].indexOf('max-age=0') > 0);
      get('http://localhost:8015/connect-examples.md', function(res, body) {
        test.ok(res.headers['cache-control'].indexOf('max-age=300') > 0);
        test.equal(res.statusCode, 200, 'should return 200');
        test.done();
      });
    });
  },
  multiple_base_directory: function(test) {
    test.expect(4);
    get({
      hostname: 'localhost',
      port: 8005,
      path: '/',
      headers: {
        accept: 'text/html'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.ok(body.indexOf('hello.txt') !== -1, 'Listing should contain hello.txt');
      get('http://localhost:8005/fixtures/hello.txt', function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'Should display contents of /fixtures/hello.txt');
        test.done();
      });
    });
  },
  livereload: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8006,
      path: '/livereload.html',
      headers: {
        accept: 'text/html'
      }
    }, function(res, body) {
      test.ok(body.indexOf('35729/livereload.js') !== -1, 'Should contain livereload snippet.');

      // check if livereload works with params
      get({
        hostname: 'localhost',
        port: 8006,
        path: '/livereload.html?a=1&b=2#id',
        headers: {
          accept: 'text/html'
        }
      }, function(res, body) {
        test.ok(body.indexOf('35729/livereload.js') !== -1, 'Should contain livereload snippet.');
        test.done();
      });
    });
  },
  custom_middleware: function(test) {
    var PORT = 8007;
    test.expect(4);
    get({
      hostname: 'localhost',
      port: PORT,
      path: '/hello/world',
      headers: {
        accept: 'text/plain'
      },
      middleware: []
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello from port ' + PORT, 'should return a dynamic response');
      get({
        hostname: 'localhost',
        port: PORT,
        path: '/fixtures/hello.txt',
        headers: {
          accept: 'text/plain'
        }
      }, function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'should return static page');
        test.done();
      });
    });
  },
  null_middleware_should_use_default_middleware: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8008,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  empty_middleware_should_404_everything: function(test) {
    test.expect(1);
    get({
      hostname: 'localhost',
      port: 8009,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 404, 'should return 404');
      test.done();
    });
  },
  custom_middleware_can_patch_default_middleware: function(test) {
    var PORT = 8010;
    test.expect(4);
    get({
      hostname: 'localhost',
      port: PORT,
      path: '/hello/world',
      headers: {
        accept: 'text/plain'
      },
      middleware: []
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello, world from port #' + PORT + '!', 'should return a dynamic response');

      get({
        hostname: 'localhost',
        port: PORT,
        path: '/fixtures/hello.txt',
        headers: {
          accept: 'text/plain'
        }
      }, function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'should return static page');
        test.done();
      });
    });
  },
  allHostname: function(test) {
    test.expect(3);

    get('http://localhost:8012/fixtures/hello.txt', function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      get('http://127.0.0.1:8012/fixtures/hello.txt', function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        get('http://0.0.0.0:8012/fixtures/hello.txt', function(res, body) {
          test.equal(res.statusCode, 200, 'should return 200');
          test.done();
        });
      });
    });
  },
  onCreateServer: function(test) {
    test.expect(1);

    get('http://localhost:8013/hello', function(res, body) {
      test.ok(grunt.config.data.connect.onCreateServer.test, 'should set configuration object on request');
      test.done();
    });
  },
  routedMiddleware: function(test) {
    test.expect(1);

    get('http://localhost:8016/mung', function(res, body) {
      test.equal(body, 'Yay', 'should return a string at /mung');
      test.done();
    });
  }
};
<commit_msg>skip hostname test on win32<commit_after>'use strict';

var grunt = require('grunt');
var http = require('http');
var https = require('https');
var http2 = require('http2');

function get(url, done) {
  var client = http;
  if (typeof url === 'object' && url.scheme === 'http2') {
    client = http2;
    delete url.scheme;
  }
  else if (typeof url === 'string' && url.toLowerCase().indexOf('https') === 0 ||
    typeof url === 'object' && url.port === 443 ||
    typeof url === 'object' && url.scheme === 'https') {
    client = https;
    delete url.scheme;
  }
  client.get(url, function(res) {
    var body = '';
    res.on('data', function(chunk) {
      body += chunk;
    }).on('end', function() {
      done(res, body);
    });
  });
}

exports.connect = {
  custom_base: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8000,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_port: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8001,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_http2: function(test) {
    if (!grunt.config.data.connect.http2) {
      test.done();
    }
    else {
      test.expect(3);
      get({
        scheme: 'http2',
        rejectUnauthorized: false,
        hostname: 'localhost',
        port: 8017,
        path: '/fixtures/hello.txt',
        headers: {
          accept: 'text/plain'
        }
      }, function(res, body) {
        test.equal(res.httpVersion, '2.0', 'should return HTTP/2 response');
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'should return static page');
        test.done();
      });
    }
  },
  custom_https: function(test) {
    test.expect(2);
    get({
      scheme: 'https',
      /* per http://nodejs.org/api/https.html#https_https_request_options_callback
         allow certs signed by an untrusted CA */
      rejectUnauthorized: false,
      hostname: 'localhost',
      port: 8002,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_https_certs: function(test) {
    test.expect(2);
    get({
      scheme: 'https',
      rejectUnauthorized: false,
      hostname: 'localhost',
      port: 8003,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  custom_base_with_options: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8014,
      path: '/',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  multiple_base: function(test) {
    test.expect(4);
    get({
      hostname: 'localhost',
      port: 8004,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(res.headers['content-type'], 'text/plain; charset=UTF-8', 'should return plaintext content type');
      get('http://localhost:8004/connect-examples.md', function(res, body) {
        test.equal(res.headers['content-type'], 'text/x-markdown; charset=UTF-8', 'should return markdown content type');
        test.equal(res.statusCode, 200, 'should return 200');
        test.done();
      });
    });
  },
  multiple_base_with_options: function(test) {
    test.expect(5);
    get({
      hostname: 'localhost',
      port: 8015,
      path: '/',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.ok(res.headers['cache-control'].indexOf('max-age=0') > 0);
      get('http://localhost:8015/connect-examples.md', function(res, body) {
        test.ok(res.headers['cache-control'].indexOf('max-age=300') > 0);
        test.equal(res.statusCode, 200, 'should return 200');
        test.done();
      });
    });
  },
  multiple_base_directory: function(test) {
    test.expect(4);
    get({
      hostname: 'localhost',
      port: 8005,
      path: '/',
      headers: {
        accept: 'text/html'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.ok(body.indexOf('hello.txt') !== -1, 'Listing should contain hello.txt');
      get('http://localhost:8005/fixtures/hello.txt', function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'Should display contents of /fixtures/hello.txt');
        test.done();
      });
    });
  },
  livereload: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8006,
      path: '/livereload.html',
      headers: {
        accept: 'text/html'
      }
    }, function(res, body) {
      test.ok(body.indexOf('35729/livereload.js') !== -1, 'Should contain livereload snippet.');

      // check if livereload works with params
      get({
        hostname: 'localhost',
        port: 8006,
        path: '/livereload.html?a=1&b=2#id',
        headers: {
          accept: 'text/html'
        }
      }, function(res, body) {
        test.ok(body.indexOf('35729/livereload.js') !== -1, 'Should contain livereload snippet.');
        test.done();
      });
    });
  },
  custom_middleware: function(test) {
    var PORT = 8007;
    test.expect(4);
    get({
      hostname: 'localhost',
      port: PORT,
      path: '/hello/world',
      headers: {
        accept: 'text/plain'
      },
      middleware: []
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello from port ' + PORT, 'should return a dynamic response');
      get({
        hostname: 'localhost',
        port: PORT,
        path: '/fixtures/hello.txt',
        headers: {
          accept: 'text/plain'
        }
      }, function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'should return static page');
        test.done();
      });
    });
  },
  null_middleware_should_use_default_middleware: function(test) {
    test.expect(2);
    get({
      hostname: 'localhost',
      port: 8008,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello world', 'should return static page');
      test.done();
    });
  },
  empty_middleware_should_404_everything: function(test) {
    test.expect(1);
    get({
      hostname: 'localhost',
      port: 8009,
      path: '/fixtures/hello.txt',
      headers: {
        accept: 'text/plain'
      }
    }, function(res, body) {
      test.equal(res.statusCode, 404, 'should return 404');
      test.done();
    });
  },
  custom_middleware_can_patch_default_middleware: function(test) {
    var PORT = 8010;
    test.expect(4);
    get({
      hostname: 'localhost',
      port: PORT,
      path: '/hello/world',
      headers: {
        accept: 'text/plain'
      },
      middleware: []
    }, function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      test.equal(body, 'Hello, world from port #' + PORT + '!', 'should return a dynamic response');

      get({
        hostname: 'localhost',
        port: PORT,
        path: '/fixtures/hello.txt',
        headers: {
          accept: 'text/plain'
        }
      }, function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        test.equal(body, 'Hello world', 'should return static page');
        test.done();
      });
    });
  },
  allHostname: function(test) {
    test.expect(3);

    get('http://localhost:8012/fixtures/hello.txt', function(res, body) {
      test.equal(res.statusCode, 200, 'should return 200');
      get('http://127.0.0.1:8012/fixtures/hello.txt', function(res, body) {
        test.equal(res.statusCode, 200, 'should return 200');
        if (process.platform === 'win32') {
          // there is a discrepancy with hostnames. Skip the test until further changes.
          test.ok(true);
          test.done();
        } else {
          get('http://0.0.0.0:8012/fixtures/hello.txt', function(res, body) {
            test.equal(res.statusCode, 200, 'should return 200');
            test.done();
          });
        }
      });
    });
  },
  onCreateServer: function(test) {
    test.expect(1);

    get('http://localhost:8013/hello', function(res, body) {
      test.ok(grunt.config.data.connect.onCreateServer.test, 'should set configuration object on request');
      test.done();
    });
  },
  routedMiddleware: function(test) {
    test.expect(1);

    get('http://localhost:8016/mung', function(res, body) {
      test.equal(body, 'Yay', 'should return a string at /mung');
      test.done();
    });
  }
};
<|endoftext|>


<commit_before>// # Local File System Image Storage module
// The (default) module for storing images, using the local file system

var _       = require('underscore'),
    express = require('express'),
    fs      = require('fs-extra'),
    nodefn  = require('when/node/function'),
    path    = require('path'),
    when    = require('when'),
    errors  = require('../errorHandling'),
    configPaths = require('../config/paths'),
    baseStore   = require('./base'),

    localFileStore;

localFileStore = _.extend(baseStore, {
    // ### Save
    // Saves the image to storage (the file system)
    // - image is the express image object
    // - returns a promise which ultimately returns the full url to the uploaded image
    'save': function (image) {
        var saved = when.defer(),
            targetDir = this.getTargetDir(configPaths().imagesRelPath),
            targetFilename;

        this.getUniqueFileName(this, image, targetDir).then(function (filename) {
            targetFilename = filename;
            return nodefn.call(fs.mkdirs, targetDir);
        }).then(function () {
            return nodefn.call(fs.copy, image.path, targetFilename);
        }).then(function () {
            return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
        }).then(function () {
            // The src for the image must be in URI format, not a file system path, which in Windows uses \
            // For local file system storage can use relative path so add a slash
            var fullUrl = (configPaths().subdir + '/' + targetFilename).replace(new RegExp('\\' + path.sep, 'g'), '/');
            return saved.resolve(fullUrl);
        }).otherwise(function (e) {
            errors.logError(e);
            return saved.reject(e);
        });

        return saved.promise;
    },

    'exists': function (filename) {
        // fs.exists does not play nicely with nodefn because the callback doesn't have an error argument
        var done = when.defer();

        fs.exists(filename, function (exists) {
            done.resolve(exists);
        });

        return done.promise;
    },

    // middleware for serving the files
    'serve': function () {
        var ONE_HOUR_MS = 60 * 60 * 1000,
            ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;

        // For some reason send divides the max age number by 1000
        return express['static'](configPaths().imagesPath, {maxAge: ONE_YEAR_MS});
    }
});

module.exports = localFileStore;
<commit_msg>[fix] use absolute paths<commit_after>// # Local File System Image Storage module
// The (default) module for storing images, using the local file system

var _       = require('underscore'),
    express = require('express'),
    fs      = require('fs-extra'),
    nodefn  = require('when/node/function'),
    path    = require('path'),
    when    = require('when'),
    errors  = require('../errorHandling'),
    configPaths = require('../config/paths'),
    baseStore   = require('./base'),

    localFileStore;

localFileStore = _.extend(baseStore, {
    // ### Save
    // Saves the image to storage (the file system)
    // - image is the express image object
    // - returns a promise which ultimately returns the full url to the uploaded image
    'save': function (image) {
        var saved = when.defer(),
            targetDir = this.getTargetDir(configPaths().imagesPath),
            targetFilename;

        this.getUniqueFileName(this, image, targetDir).then(function (filename) {
            targetFilename = filename;
            return nodefn.call(fs.mkdirs, targetDir);
        }).then(function () {
            return nodefn.call(fs.copy, image.path, targetFilename);
        }).then(function () {
            return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
        }).then(function () {
            // The src for the image must be in URI format, not a file system path, which in Windows uses \
            // For local file system storage can use relative path so add a slash
            var fullUrl = (configPaths().subdir + '/' + path.relative(configPaths().appRoot, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');
            return saved.resolve(fullUrl);
        }).otherwise(function (e) {
            errors.logError(e);
            return saved.reject(e);
        });

        return saved.promise;
    },

    'exists': function (filename) {
        // fs.exists does not play nicely with nodefn because the callback doesn't have an error argument
        var done = when.defer();

        fs.exists(filename, function (exists) {
            done.resolve(exists);
        });

        return done.promise;
    },

    // middleware for serving the files
    'serve': function () {
        var ONE_HOUR_MS = 60 * 60 * 1000,
            ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;

        // For some reason send divides the max age number by 1000
        return express['static'](configPaths().imagesPath, {maxAge: ONE_YEAR_MS});
    }
});

module.exports = localFileStore;
<|endoftext|>


<commit_before>import 'babel-core/register'
import test from 'ava'
import testData from './testData.json'
import request from 'supertest'
import app from '../../index'
import Course from '../../api/courses/model'

test.before('setup', t => {
  // Drop all documents
  Course.remove({}, err => {
    if (err) t.fail(err.message)
    // Insert test data
    Course.create(testData, err => {
      if (err) t.fail(err.message)
      t.end()
    })
  })
})

/* list tests */

test('/list', t => {
  request(app)
    .get('/1.0/courses/list')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(0, 10)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?limit=0', t => {
  request(app)
    .get('/1.0/courses/list?limit=0')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?limit=2', t => {
  request(app)
    .get('/1.0/courses/list?limit=2')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(0, 2)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?limit=200', t => {
  request(app)
    .get('/1.0/courses/list?limit=200')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?skip=10', t => {
  request(app)
    .get('/1.0/courses/list?skip=10')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(10, 20)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?skip=200', t => {
  request(app)
    .get('/1.0/courses/list?skip=200')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect('[]')
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?skip=2&limit=2', t => {
  request(app)
    .get('/1.0/courses/list?skip=2&limit=2')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(2, 4)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

/* show tests */

test(`/${testData[0].id}`, t => {
  request(app)
    .get(`/1.0/courses/${testData[0].id}`)
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData[0]))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/XYZ789H1F20159', t => {
  request(app)
    .get('/1.0/courses/XYZ789H1F20159')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

/* search tests */

test('/search?q=', t => {
  request(app)
    .get('/1.0/courses/search?q=')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/search?q=%22recreational%20space%20and%20more%22', t => {
  request(app)
    .get('/1.0/courses/search?q=%22recreational%20space%20and%20more%22')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.filter(doc => { return doc.description.match('recreational space and more') })))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/search?q=loremipsumdolorsitamet', t => {
  request(app)
    .get('/1.0/courses/search?q=loremipsumdolorsitamet')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect('[]')
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

/* TODO: filter tests (fun) */

test('/filter?q=', t => {
  request(app)
    .get('/1.0/courses/filter?q=')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test.after('cleanup', t => {
  Course.remove({}, err => {
    if (err) t.fail(err.message)
    t.end()
  })
})
<commit_msg>Debug travisCI<commit_after>import 'babel-core/register'
import test from 'ava'
import testData from './testData.json'
import request from 'supertest'
import app from '../../index'
import Course from '../../api/courses/model'

test.before('setup', t => {
  // Drop all documents
  Course.remove({}, err => {
    if (err) t.fail(err.message)
    // Insert test data
    Course.create(testData, err => {
      if (err) t.fail(err.message)
      t.end()
    })
  })
})

/* list tests */

test('/list', t => {
  request(app)
    .get('/1.0/courses/list')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(0, 10)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?limit=0', t => {
  request(app)
    .get('/1.0/courses/list?limit=0')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?limit=2', t => {
  request(app)
    .get('/1.0/courses/list?limit=2')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(0, 2)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?limit=200', t => {
  request(app)
    .get('/1.0/courses/list?limit=200')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?skip=10', t => {
  request(app)
    .get('/1.0/courses/list?skip=10')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(10, 20)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?skip=200', t => {
  request(app)
    .get('/1.0/courses/list?skip=200')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect('[]')
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/list?skip=2&limit=2', t => {
  request(app)
    .get('/1.0/courses/list?skip=2&limit=2')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.slice(2, 4)))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

/* show tests */

test(`/${testData[0].id}`, t => {
  request(app)
    .get(`/1.0/courses/${testData[0].id}`)
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData[0]))
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/XYZ789H1F20159', t => {
  request(app)
    .get('/1.0/courses/XYZ789H1F20159')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

/* search tests */

test('/search?q=', t => {
  request(app)
    .get('/1.0/courses/search?q=')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/search?q=%22recreational%20space%20and%20more%22', t => {
  request(app)
    .get('/1.0/courses/search?q=%22recreational%20space%20and%20more%22')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect(JSON.stringify(testData.filter(doc => { return doc.description.match('recreational space and more') })))
    .end((err, res) => {
      console.log(res)
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test('/search?q=loremipsumdolorsitamet', t => {
  request(app)
    .get('/1.0/courses/search?q=loremipsumdolorsitamet')
    .expect('Content-Type', /json/)
    .expect(200)
    .expect('[]')
    .end((err, res) => {
      console.log(res)
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

/* TODO: filter tests (fun) */

test('/filter?q=', t => {
  request(app)
    .get('/1.0/courses/filter?q=')
    .expect('Content-Type', /json/)
    .expect(400)
    .end((err, res) => {
      if (err) t.fail(err.message)
      t.pass()
      t.end()
    })
})

test.after('cleanup', t => {
  Course.remove({}, err => {
    if (err) t.fail(err.message)
    t.end()
  })
})
<|endoftext|>


<commit_before>

function resetStatisticsPage() {
	fhqgui.filter.stats.page = 0;
}

function setStatisticsPage(val) {
	fhqgui.filter.stats.page = val;
}

function createPageStatistics(gameid) {
	fhqgui.setFilter('stats');
	var cp = new FHQContentPage();
	cp.clear();
	cp.append('Found: 0
'
		+ '

');
}

function updateStatistics() {

	var ls = document.getElementById("listStatistics");
	ls.innerHTML = "Loading...";

	var params = fhqgui.filter.stats.getParams();
	send_request_post(
		'api/statistics/list.php',
		createUrlFromObj(params),
		function (obj) {
			if (obj.result == "fail") {
				ls.innerHTML = obj.error.message;
			} else {

				var found = parseInt(obj.data.count, 10);
				document.getElementById("statistics_found").innerHTML = found;
				var onpage = parseInt(obj.data.onpage, 10);
				var page = parseInt(obj.data.page, 10);

				var content = '';
				// content += 'Processed ' + obj.lead_time_sec + ' sec
';
				content += fhqgui.paginator(0, found, onpage, page, 'setStatisticsPage', 'fhqgui.updatePageSkills()');
				var tbl = new FHQTable();
				tbl.openrow();
				tbl.cell('Quest');
				tbl.cell('Chart');
				tbl.cell('Users who solved quest');
				tbl.closerow();
				for (var k in obj.data.quests) {
					if (obj.data.quests.hasOwnProperty(k)) {
						var q = obj.data.quests[k];
						tbl.openrow();
						tbl.cell(fhqgui.questIcon(q.id, q.name, q.subject, q.score, q.solved));
						tbl.cell('<canvas id="questChart' + q.id + '" width="300" height="200"></canvas>');
						var usrs = [];
						for (var u in q.users) {
							usrs.push(fhqgui.userIcon(q.users[u].userid, q.users[u].logo, q.users[u].nick));
						}
						tbl.cell(usrs.join(" "));
						tbl.closerow();
					}
				}
				content += tbl.render();

				ls.innerHTML = content;

				var options = {
					segmentShowStroke : true,
					segmentStrokeColor : "#606060",
					segmentStrokeWidth : 1,
					percentageInnerCutout : 35, // This is 0 for Pie charts
					animationSteps : 100,
					animationEasing : "easeOutBounce",
					animateRotate : false,
					animateScale : false,
					legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%>
<span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%>
<%}%>
"
				};

				// update charts
				for (var k in obj.data.quests) {
					if (obj.data.quests.hasOwnProperty(k)) {
						var q = obj.data.quests[k];
						var chartid = 'questChart' + q.id;
						var data = [
							{
								value: q.solved,
								color: "#9f9f9f",
								highlight: "#606060",
								label: "Solved"
							},
							{
								value: q.tries_solved,
								color: "#9f9f9f",
								highlight: "#606060",
								label: "Tried (who solved)"
							},
							{
								value: q.tries_nosolved,
								color:"#9f9f9f",
								highlight: "#606060",
								label: "Tried (who didn't solve)"
							}
						];
						var ctx = document.getElementById(chartid).getContext("2d");
						var myNewChart = new Chart(ctx).Doughnut(data, options);
					}
				}
			}
		}
	);
}

function hatchAnswer(answer) {
	hatch = "";
	for (var i = 0; i < answer.length; i++) {
		hatch += "*";
	}
	return '
' + hatch + "
";
}

function createPageAnswerList() {
	fhqgui.setFilter('answerlist');
	var cp = document.getElementById('content_page');
	cp.innerHTML = '';

	var content = '';
	content += 'Found: 0
';
	content += '

'; // fhqparamtbl
	content += '

';
	cp.innerHTML = content;
}

function resetPageAnswerList() {
	fhqgui.filter.answerlist.page = 0;
}

function setPageAnswerList(val) {
	fhqgui.filter.answerlist.page = val;
}

function updateAnswerList() {

	var al = document.getElementById("answerList");
	fhqgui.closeModalDialog();

	al.innerHTML = "Loading...";

	fhq.statistics.answerlist(
		fhqgui.filter.answerlist.getParams(),
		function (obj) {
			if (obj.result == "fail") {
				el.innerHTML = obj.error.message;
			} else {
				var found = parseInt(obj.data.count, 10);
				document.getElementById("answerlist_found").innerHTML = found;
				var onpage = parseInt(obj.data.onpage, 10);
				var page = parseInt(obj.data.page, 10);

				al.innerHTML = '
' + fhqgui.paginator(0,found, onpage, page, 'setPageAnswerList', 'updateAnswerList') + '
';

				var tbl = new FHQTable();
				tbl.openrow();
				tbl.cell('Date Time / Game');
				tbl.cell('Quest');
				tbl.cell('Answer');
				tbl.cell('Passed');
				tbl.cell('User');
				tbl.closerow();

				for (var k in obj.data.answers) {
					content += '';
					if (obj.data.answers.hasOwnProperty(k)) {
						var ans = obj.data.answers[k];
						if (ans.levenshtein == 0)
							tbl.openrow('fhqrow_yellow');
						else if (ans.levenshtein > 0 && ans.levenshtein < 4)
							tbl.openrow('fhqrow_red');
						else
							tbl.openrow('');

						tbl.cell(ans.dt + ' / ' + ans.game.title);
						tbl.cell(fhqgui.questIcon(ans.quest.id, ans.quest.name, ans.quest.subject, ans.quest.score, ans.quest.solved));
						tbl.cell('Try:
' + (ans.passed == 'Yes' ? hatchAnswer(ans.answer_try) : ans.answer_try)
							+ '

Real:
' + hatchAnswer(ans.answer_real)
							+ '
Levenstein: ' + ans.levenshtein
						);
						tbl.cell(ans.passed);
						tbl.cell(fhqgui.userIcon(ans.user.id, ans.user.logo, ans.user.nick));
						tbl.closerow();

					}
				}
				content = tbl.render();
				al.innerHTML += content;
			}
		}
	);
}
<commit_msg>fixed stat page<commit_after>

function resetStatisticsPage() {
	fhqgui.filter.stats.page = 0;
}

function setStatisticsPage(val) {
	fhqgui.filter.stats.page = val;
}

function createPageStatistics(gameid) {
	fhqgui.setFilter('stats');
	var cp = new FHQContentPage();
	cp.clear();
	cp.append('Found: 0
'
		+ '

');
}

function updateStatistics() {

	var ls = document.getElementById("listStatistics");
	ls.innerHTML = "Loading...";

	var params = fhqgui.filter.stats.getParams();
	send_request_post(
		'api/statistics/list.php',
		createUrlFromObj(params),
		function (obj) {
			if (obj.result == "fail") {
				ls.innerHTML = obj.error.message;
			} else {

				var found = parseInt(obj.data.count, 10);
				document.getElementById("statistics_found").innerHTML = found;
				var onpage = parseInt(obj.data.onpage, 10);
				var page = parseInt(obj.data.page, 10);

				var content = '';
				// content += 'Processed ' + obj.lead_time_sec + ' sec
';
				content += fhqgui.paginator(0, found, onpage, page, 'setStatisticsPage', 'updateStatistics');
				var tbl = new FHQTable();
				tbl.openrow();
				tbl.cell('Quest');
				tbl.cell('Chart');
				tbl.cell('Users who solved quest');
				tbl.closerow();
				for (var k in obj.data.quests) {
					if (obj.data.quests.hasOwnProperty(k)) {
						var q = obj.data.quests[k];
						tbl.openrow();
						tbl.cell(fhqgui.questIcon(q.id, q.name, q.subject, q.score, q.solved));
						tbl.cell('<canvas id="questChart' + q.id + '" width="300" height="200"></canvas>');
						var usrs = [];
						for (var u in q.users) {
							usrs.push(fhqgui.userIcon(q.users[u].userid, q.users[u].logo, q.users[u].nick));
						}
						tbl.cell(usrs.join(" "));
						tbl.closerow();
					}
				}
				content += tbl.render();

				ls.innerHTML = content;

				var options = {
					segmentShowStroke : true,
					segmentStrokeColor : "#606060",
					segmentStrokeWidth : 1,
					percentageInnerCutout : 35, // This is 0 for Pie charts
					animationSteps : 100,
					animationEasing : "easeOutBounce",
					animateRotate : false,
					animateScale : false,
					legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%>
<span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%>
<%}%>
"
				};

				// update charts
				for (var k in obj.data.quests) {
					if (obj.data.quests.hasOwnProperty(k)) {
						var q = obj.data.quests[k];
						var chartid = 'questChart' + q.id;
						var data = [
							{
								value: q.solved,
								color: "#9f9f9f",
								highlight: "#606060",
								label: "Solved"
							},
							{
								value: q.tries_solved,
								color: "#9f9f9f",
								highlight: "#606060",
								label: "Tried (who solved)"
							},
							{
								value: q.tries_nosolved,
								color:"#9f9f9f",
								highlight: "#606060",
								label: "Tried (who didn't solve)"
							}
						];
						var ctx = document.getElementById(chartid).getContext("2d");
						var myNewChart = new Chart(ctx).Doughnut(data, options);
					}
				}
			}
		}
	);
}

function hatchAnswer(answer) {
	hatch = "";
	for (var i = 0; i < answer.length; i++) {
		hatch += "*";
	}
	return '
' + hatch + "
";
}

function createPageAnswerList() {
	fhqgui.setFilter('answerlist');
	var cp = document.getElementById('content_page');
	cp.innerHTML = '';

	var content = '';
	content += 'Found: 0
';
	content += '

'; // fhqparamtbl
	content += '

';
	cp.innerHTML = content;
}

function resetPageAnswerList() {
	fhqgui.filter.answerlist.page = 0;
}

function setPageAnswerList(val) {
	fhqgui.filter.answerlist.page = val;
}

function updateAnswerList() {

	var al = document.getElementById("answerList");
	fhqgui.closeModalDialog();

	al.innerHTML = "Loading...";

	fhq.statistics.answerlist(
		fhqgui.filter.answerlist.getParams(),
		function (obj) {
			if (obj.result == "fail") {
				el.innerHTML = obj.error.message;
			} else {
				var found = parseInt(obj.data.count, 10);
				document.getElementById("answerlist_found").innerHTML = found;
				var onpage = parseInt(obj.data.onpage, 10);
				var page = parseInt(obj.data.page, 10);

				al.innerHTML = '
' + fhqgui.paginator(0,found, onpage, page, 'setPageAnswerList', 'updateAnswerList') + '
';

				var tbl = new FHQTable();
				tbl.openrow();
				tbl.cell('Date Time / Game');
				tbl.cell('Quest');
				tbl.cell('Answer');
				tbl.cell('Passed');
				tbl.cell('User');
				tbl.closerow();

				for (var k in obj.data.answers) {
					content += '';
					if (obj.data.answers.hasOwnProperty(k)) {
						var ans = obj.data.answers[k];
						if (ans.levenshtein == 0)
							tbl.openrow('fhqrow_yellow');
						else if (ans.levenshtein > 0 && ans.levenshtein < 4)
							tbl.openrow('fhqrow_red');
						else
							tbl.openrow('');

						tbl.cell(ans.dt + ' / ' + ans.game.title);
						tbl.cell(fhqgui.questIcon(ans.quest.id, ans.quest.name, ans.quest.subject, ans.quest.score, ans.quest.solved));
						tbl.cell('Try:
' + (ans.passed == 'Yes' ? hatchAnswer(ans.answer_try) : ans.answer_try)
							+ '

Real:
' + hatchAnswer(ans.answer_real)
							+ '
Levenstein: ' + ans.levenshtein
						);
						tbl.cell(ans.passed);
						tbl.cell(fhqgui.userIcon(ans.user.id, ans.user.logo, ans.user.nick));
						tbl.closerow();

					}
				}
				content = tbl.render();
				al.innerHTML += content;
			}
		}
	);
}
<|endoftext|>


<commit_before>/**
 * Module dependencies
 */

var _ = require('@sailshq/lodash');

/**
 * Transformation
 *
 * Allows for a Waterline Collection to have different
 * attributes than what actually exist in an adater's representation.
 *
 * @param {Object} attributes
 * @param {Object} tables
 */

var Transformation = module.exports = function(attributes) {

  // Hold an internal mapping of keys to transform
  this._transformations = {};

  // Initialize
  this.initialize(attributes);

  return this;
};

/**
 * Initial mapping of transformations.
 *
 * @param {Object} attributes
 * @param {Object} tables
 */

Transformation.prototype.initialize = function(attributes) {
  var self = this;

  _.each(attributes, function(wlsAttrDef, attrName) {
    // Make sure the attribute has a columnName set
    if (!_.has(wlsAttrDef, 'columnName')) {
      return;
    }

    // Ensure the columnName is a string
    if (!_.isString(wlsAttrDef.columnName)) {
      throw new Error('Consistency violation: `columnName` must be a string.  But for this attribute (`'+attrName+'`) it is not!');
    }

    // Set the column name transformation
    self._transformations[attrName] = wlsAttrDef.columnName;
  });
};

/**
 * Transforms a set of attributes into a representation used
 * in an adapter.
 *
 * @param {Object} attributes to transform
 * @return {Object}
 */

Transformation.prototype.serialize = function(values, behavior) {
  var self = this;

  behavior = behavior || 'default';

  function recursiveParse(obj) {

    // Return if no object
    if (!obj) {
      return;
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // TODO: remove this:
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Handle array of types for findOrCreateEach
    if (_.isString(obj)) {
      if (_.has(self._transformations, obj)) {
        values = self._transformations[obj];
        return;
      }

      return;
    }
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    _.each(obj, function(propertyValue, propertyName) {
      // Schema must be serialized in first level only
      if (behavior === 'schema') {
        if (_.has(self._transformations, propertyName)) {
          obj[self._transformations[propertyName]] = propertyValue;
          delete obj[propertyName];
        }
        return;
      }

      // Recursively parse `OR` or `AND` criteria objects to transform keys
      if (_.isArray(propertyValue) && (propertyName === 'or' || propertyName === 'and')) {
        return recursiveParse(propertyValue);
      }

      // If nested dictionary, then take the recursive step, calling the function again
      // and passing the nested dictionary as `obj`
      if (!_.isDate(propertyValue) && _.isPlainObject(propertyValue)) {

        // check if object key is in the transformations
        if (_.has(self._transformations, propertyName)) {
          obj[self._transformations[propertyName]] = propertyValue;

          // Only delete if the names are different
          if (self._transformations[propertyName] !== propertyName) {
            delete obj[propertyName];
          }

          return recursiveParse(obj[self._transformations[propertyName]]);
        }

        return recursiveParse(propertyValue);
      }

      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // TODO: Assuming this is still in use, then split it up into separate
      // utilities (consider what would happen if you named an attribute "select"
      // or "sort").  We shouldn't use the same logic to transform attrs to column
      // names in criteria as we do `newRecord` or `valuesToSet`, etc.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // If the property === SELECT check for any transformation keys
      if (propertyName === 'select' && _.isArray(propertyValue)) {
        // var arr = _.clone(obj[property]);
        _.each(propertyValue, function(prop) {
          if(_.has(self._transformations, prop)) {
            var idx = _.indexOf(propertyValue, prop);
            if(idx > -1) {
              obj[propertyName][idx] = self._transformations[prop];
            }
          }
        });
      }

      // If the property === SORT check for any transformation keys
      if (propertyName === 'sort' && _.isArray(propertyValue)) {
        obj.sort = _.map(obj.sort, function(sortClause) {
          var sort = {};
          var attrName = _.first(_.keys(sortClause));
          var sortDirection = sortClause[attrName];
          var columnName = self._transformations[attrName];
          sort[columnName] = sortDirection;
          return sort;
        });
      }
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      // Check if property is a transformation key
      if (_.has(self._transformations, propertyName)) {
        obj[self._transformations[propertyName]] = propertyValue;
        if (self._transformations[propertyName] !== propertyName) {
          delete obj[propertyName];
        }
      }
    });
  }

  // Recursivly parse attributes to handle nested criteria
  recursiveParse(values);

  return values;
};

/**
 * .unserialize()
 *
 * Destructively transforms a physical-layer record received
 * from an adapter into a logical representation appropriate
 * for userland (i.e. swapping out column names for attribute
 * names)
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 * @param {Dictionary} pRecord
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 * @returns {Dictionary}
 *          This is an unnecessary return -- this method just
 *          returns the same reference to the original pRecord,
 *          which has been destructively mutated anyway.
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

Transformation.prototype.unserialize = function(pRecord) {

  // Loop through the keys of the record and change them.
  _.each(this._transformations, function(columnName, attrName) {

    if (!_.has(pRecord, columnName)) {
      return;
    }

    pRecord[attrName] = pRecord[columnName];
    if (columnName !== attrName) {
      delete pRecord[columnName];
    }

  });

  return pRecord;
};
<commit_msg>only remove properties if the names have actually been transformed<commit_after>/**
 * Module dependencies
 */

var _ = require('@sailshq/lodash');

/**
 * Transformation
 *
 * Allows for a Waterline Collection to have different
 * attributes than what actually exist in an adater's representation.
 *
 * @param {Object} attributes
 * @param {Object} tables
 */

var Transformation = module.exports = function(attributes) {

  // Hold an internal mapping of keys to transform
  this._transformations = {};

  // Initialize
  this.initialize(attributes);

  return this;
};

/**
 * Initial mapping of transformations.
 *
 * @param {Object} attributes
 * @param {Object} tables
 */

Transformation.prototype.initialize = function(attributes) {
  var self = this;

  _.each(attributes, function(wlsAttrDef, attrName) {
    // Make sure the attribute has a columnName set
    if (!_.has(wlsAttrDef, 'columnName')) {
      return;
    }

    // Ensure the columnName is a string
    if (!_.isString(wlsAttrDef.columnName)) {
      throw new Error('Consistency violation: `columnName` must be a string.  But for this attribute (`'+attrName+'`) it is not!');
    }

    // Set the column name transformation
    self._transformations[attrName] = wlsAttrDef.columnName;
  });
};

/**
 * Transforms a set of attributes into a representation used
 * in an adapter.
 *
 * @param {Object} attributes to transform
 * @return {Object}
 */

Transformation.prototype.serialize = function(values, behavior) {
  var self = this;

  behavior = behavior || 'default';

  function recursiveParse(obj) {

    // Return if no object
    if (!obj) {
      return;
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // TODO: remove this:
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Handle array of types for findOrCreateEach
    if (_.isString(obj)) {
      if (_.has(self._transformations, obj)) {
        values = self._transformations[obj];
        return;
      }

      return;
    }
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    _.each(obj, function(propertyValue, propertyName) {
      // Schema must be serialized in first level only
      if (behavior === 'schema') {
        if (_.has(self._transformations, propertyName)) {
          obj[self._transformations[propertyName]] = propertyValue;

          // Only delete if the names are different
          if (self._transformations[propertyName] !== propertyName) {
            delete obj[propertyName];
          }
        }
        return;
      }

      // Recursively parse `OR` or `AND` criteria objects to transform keys
      if (_.isArray(propertyValue) && (propertyName === 'or' || propertyName === 'and')) {
        return recursiveParse(propertyValue);
      }

      // If nested dictionary, then take the recursive step, calling the function again
      // and passing the nested dictionary as `obj`
      if (!_.isDate(propertyValue) && _.isPlainObject(propertyValue)) {

        // check if object key is in the transformations
        if (_.has(self._transformations, propertyName)) {
          obj[self._transformations[propertyName]] = propertyValue;

          // Only delete if the names are different
          if (self._transformations[propertyName] !== propertyName) {
            delete obj[propertyName];
          }

          return recursiveParse(obj[self._transformations[propertyName]]);
        }

        return recursiveParse(propertyValue);
      }

      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // TODO: Assuming this is still in use, then split it up into separate
      // utilities (consider what would happen if you named an attribute "select"
      // or "sort").  We shouldn't use the same logic to transform attrs to column
      // names in criteria as we do `newRecord` or `valuesToSet`, etc.
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // If the property === SELECT check for any transformation keys
      if (propertyName === 'select' && _.isArray(propertyValue)) {
        // var arr = _.clone(obj[property]);
        _.each(propertyValue, function(prop) {
          if(_.has(self._transformations, prop)) {
            var idx = _.indexOf(propertyValue, prop);
            if(idx > -1) {
              obj[propertyName][idx] = self._transformations[prop];
            }
          }
        });
      }

      // If the property === SORT check for any transformation keys
      if (propertyName === 'sort' && _.isArray(propertyValue)) {
        obj.sort = _.map(obj.sort, function(sortClause) {
          var sort = {};
          var attrName = _.first(_.keys(sortClause));
          var sortDirection = sortClause[attrName];
          var columnName = self._transformations[attrName];
          sort[columnName] = sortDirection;
          return sort;
        });
      }
      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      // Check if property is a transformation key
      if (_.has(self._transformations, propertyName)) {
        obj[self._transformations[propertyName]] = propertyValue;
        if (self._transformations[propertyName] !== propertyName) {
          delete obj[propertyName];
        }
      }
    });
  }

  // Recursivly parse attributes to handle nested criteria
  recursiveParse(values);

  return values;
};

/**
 * .unserialize()
 *
 * Destructively transforms a physical-layer record received
 * from an adapter into a logical representation appropriate
 * for userland (i.e. swapping out column names for attribute
 * names)
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 * @param {Dictionary} pRecord
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 * @returns {Dictionary}
 *          This is an unnecessary return -- this method just
 *          returns the same reference to the original pRecord,
 *          which has been destructively mutated anyway.
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

Transformation.prototype.unserialize = function(pRecord) {

  // Loop through the keys of the record and change them.
  _.each(this._transformations, function(columnName, attrName) {

    if (!_.has(pRecord, columnName)) {
      return;
    }

    pRecord[attrName] = pRecord[columnName];
    if (columnName !== attrName) {
      delete pRecord[columnName];
    }

  });

  return pRecord;
};
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');

var Quota = require('../quota.js');
var Rule = require('../rule.js');

var generalRuleQPD = null,
    generalRuleQPS = null;

/**
 * Quota Preset for Google Analytics' Reporting and Configuration APIs
 *
 * Quota rules based on: https://developers.google.com/analytics/devguides/reporting/core/v3/limits-quotas
 * Reporting APIs docs: https://developers.google.com/analytics/devguides/reporting/
 * Configuration APIs docs: https://developers.google.com/analytics/devguides/config/
 *
 * @param options
 * @returns {Quota}
 */
module.exports = function (options) {

    // TODO: Note: Daily quotas refresh at midnight PST.

    _.defaults(options, {
        sharedIPAdress: false,
        dailyRequests: 50000,
        dailyWrites: 500,
        queriesPerSecond: 1,
        api: '',
        stickySessions: false
    });

    var quota = new Quota({
        //cancelAfter: 1000,
        //throttling: 'cutoff',
        //backoff: 'timeout'
    });

    // General Quota Limits (All APIs)

    // 50,000 requests per project per day – can be increased
    if (generalRuleQPD === null) {
        generalRuleQPD = new Rule({
            quota: options.dailyRequests,
            every: 24*60*60*1000,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: [],
            resources: ['requests']
        });
    }
    quota.addRule(generalRuleQPD);

    // 10 queries per second (QPS) per IP
    // By default, it is set to 1 query per second (QPS) and can be adjusted to a maximum value of 10.
    // Adjustment can be done here: https://console.developers.google.com/project?authuser=2
    if (generalRuleQPS === null) {

        if (options.sharedIPAdress) {

            generalRuleQPS = new Rule({
                quota: options.queriesPerSecond,
                every: 1000,
                queueing: 'memory',
                systemOfRecord: 'database',
                scope: ['userId', 'ipAddress'],
                resources: ['requests']
            });

        } else {

            generalRuleQPS = new Rule({
                quota: options.queriesPerSecond,
                every: 1000,
                queueing: 'memory',
                systemOfRecord: options.stickySessions ? 'self' : 'database',
                scope: ['userId'],
                resources: ['requests']
            });

        }

    }
    quota.addRule(generalRuleQPS);

    if (options.api === 'management') {

        // Management API - Write Requests
        // 500 write requests per project per day – can be increased
        quota.addRule({
            quota: options.dailyWrites,
            every: 24*60*60*1000,
            queueing: 'none',
            systemOfRecord: options.stickySessions ? 'self' : 'database',
            scope: [],
            resources: ['writeRequests']
        });

        // Management API - Data Import

        // 10 GB per property
        quota.addRule({
            quota: 10*1024*1024*1024,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['userId', 'propertyId'],
            resources: ['bytes']
        });

        // 10 GB per data set
        quota.addRule({
            quota: 10*1024*1024*1024,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['userId', 'datasetId'],
            resources: ['bytes']
        });

        // 50 Data Sets per property
        quota.addRule({
            quota: 50,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['userId', 'propertyId'],
            resources: ['datasets']
        });

        // 50 upload operations per property per day
        quota.addRule({
            quota: 50,
            every: 24*60*60*1000,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['userId', 'propertyId'],
            resources: ['writeRequests']
        });

        // 100 MB per date (ga:date) per data set
        quota.addRule({
            quota: 100*1024*1024,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['userId', 'datasetId', 'date'],
            resources: ['bytes']
        });

        // Management API - Unsampled Reports
        // 100 unsampled reports per day per property
        quota.addRule({
            quota: 100,
            every: 24*60*60*1000,
            queueing: 'none',
            systemOfRecord: options.stickySessions ? 'self' : 'database',
            scope: ['userId', 'propertyId'],
            resources: ['unsampledReports']
        });

    }

    if (options.api === 'provisioning') {

        // Provisioning API - Write Requests
        // 50 requests per project per day
        quota.addRule({
            quota: 50,
            every: 24*60*60*1000,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: [],
            resources: ['writeRequests']
        });

    }

    if (options.api === 'core-reporting' || options.api === 'real-time-reporting') {

        // Core Reporting API and Real Time Reporting API

        // 10,000 requests per view (profile) per day
        quota.addRule({
            quota: 10000,
            every: 24*60*60*1000,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['viewId'],
            resources: ['requests']
        });

        // 10 concurrent requests per view (profile)
        quota.addRule({
            quota: 10,
            concurrent: true,
            queueing: 'memory',
            systemOfRecord: 'database',
            scope: ['viewId'],
            resources: ['requests']
        });

    }

    if (options.api === 'mcf-reporting') {

        // Multi-channel Funnel Reporting API

        // 10 concurrent requests per view (profile)
        quota.addRule({
            quota: 10,
            concurrent: true,
            queueing: 'memory',
            systemOfRecord: 'database',
            scope: ['viewId'],
            resources: ['requests']
        });

    }

    return quota;

};
<commit_msg>Refactoring<commit_after>'use strict';

var _ = require('lodash');

var Quota = require('../quota.js');
var Rule = require('../rule.js');

var generalRuleQPD = null,
    generalRuleQPS = null;

/**
 * Quota Preset for Google Analytics' Reporting and Configuration APIs
 *
 * Quota rules based on: https://developers.google.com/analytics/devguides/reporting/core/v3/limits-quotas
 * Reporting APIs docs: https://developers.google.com/analytics/devguides/reporting/
 * Configuration APIs docs: https://developers.google.com/analytics/devguides/config/
 *
 * @param options
 * @returns {Quota}
 */
module.exports = function (options) {

    // TODO: Note: Daily quotas refresh at midnight PST.

    _.defaults(options, {
        sharedIPAdress: false,
        dailyRequests: 50000,
        dailyWrites: 500,
        queriesPerSecond: 1,
        api: '',
        stickySessions: false
    });

    var quota = new Quota({
        //cancelAfter: 1000,
        //throttling: 'cutoff',
        //backoff: 'timeout'
    });

    // General Quota Limits (All APIs)

    // 50,000 requests per project per day – can be increased
    if (generalRuleQPD === null) {
        generalRuleQPD = new Rule({
            quota: options.dailyRequests,
            every: 24*60*60*1000,
            resources: ['requests'],
            scope: [],
            queueing: 'none',
            systemOfRecord: 'database'
        });
    }
    quota.addRule(generalRuleQPD);

    // 10 queries per second (QPS) per IP
    // By default, it is set to 1 query per second (QPS) and can be adjusted to a maximum value of 10.
    // Adjustment can be done here: https://console.developers.google.com/project?authuser=2
    if (generalRuleQPS === null) {

        if (options.sharedIPAdress) {

            generalRuleQPS = new Rule({
                quota: options.queriesPerSecond,
                every: 1000,
                resources: ['requests'],
                scope: ['userId', 'ipAddress'],
                queueing: 'memory',
                systemOfRecord: 'database'
            });

        } else {

            generalRuleQPS = new Rule({
                quota: options.queriesPerSecond,
                every: 1000,
                resources: ['requests'],
                scope: ['userId'],
                queueing: 'memory',
                systemOfRecord: options.stickySessions ? 'self' : 'database'
            });

        }

    }
    quota.addRule(generalRuleQPS);

    if (options.api === 'management') {

        // Management API - Write Requests
        // 500 write requests per project per day – can be increased
        quota.addRule({
            quota: options.dailyWrites,
            every: 24*60*60*1000,
            resources: ['writeRequests'],
            scope: [],
            queueing: 'none',
            systemOfRecord: options.stickySessions ? 'self' : 'database'
        });

        // Management API - Data Import

        // 10 GB per property
        quota.addRule({
            quota: 10*1024*1024*1024,
            queueing: 'none',
            systemOfRecord: 'database',
            scope: ['userId', 'propertyId'],
            resources: ['bytes']
        });

        // 10 GB per data set
        quota.addRule({
            quota: 10*1024*1024*1024,
            resources: ['bytes'],
            scope: ['userId', 'datasetId'],
            queueing: 'none',
            systemOfRecord: 'database'
        });

        // 50 Data Sets per property
        quota.addRule({
            quota: 50,
            resources: ['datasets'],
            scope: ['userId', 'propertyId'],
            queueing: 'none',
            systemOfRecord: 'database'
        });

        // 50 upload operations per property per day
        quota.addRule({
            quota: 50,
            every: 24*60*60*1000,
            resources: ['writeRequests'],
            scope: ['userId', 'propertyId'],
            queueing: 'none',
            systemOfRecord: 'database'
        });

        // 100 MB per date (ga:date) per data set
        quota.addRule({
            quota: 100*1024*1024,
            resources: ['bytes'],
            scope: ['userId', 'datasetId', 'date'],
            queueing: 'none',
            systemOfRecord: 'database'
        });

        // Management API - Unsampled Reports
        // 100 unsampled reports per day per property
        quota.addRule({
            quota: 100,
            every: 24*60*60*1000,
            resources: ['unsampledReports'],
            scope: ['userId', 'propertyId'],
            queueing: 'none',
            systemOfRecord: options.stickySessions ? 'self' : 'database'
        });

    }

    if (options.api === 'provisioning') {

        // Provisioning API - Write Requests
        // 50 requests per project per day
        quota.addRule({
            quota: 50,
            every: 24*60*60*1000,
            resources: ['writeRequests'],
            scope: [],
            queueing: 'none',
            systemOfRecord: 'database'
        });

    }

    if (options.api === 'core-reporting' || options.api === 'real-time-reporting') {

        // Core Reporting API and Real Time Reporting API

        // 10,000 requests per view (profile) per day
        quota.addRule({
            quota: 10000,
            every: 24*60*60*1000,
            resources: ['requests'],
            scope: ['viewId'],
            queueing: 'none',
            systemOfRecord: 'database'
        });

        // 10 concurrent requests per view (profile)
        quota.addRule({
            quota: 10,
            concurrent: true,
            resources: ['requests'],
            scope: ['viewId'],
            queueing: 'memory',
            systemOfRecord: 'database'
        });

    }

    if (options.api === 'mcf-reporting') {

        // Multi-channel Funnel Reporting API

        // 10 concurrent requests per view (profile)
        quota.addRule({
            quota: 10,
            concurrent: true,
            resources: ['requests'],
            scope: ['viewId'],
            queueing: 'memory',
            systemOfRecord: 'database'
        });

    }

    return quota;

};
<|endoftext|>