<commit_before>/*
  This file is part of the Jasmine JSReporter project from Ivan De Marino.

  Copyright (C) 2011-2014 Ivan De Marino <http://ivandemarino.me>
  Copyright (C) 2014 Alex Treppass <http://alextreppass.co.uk>

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL IVAN DE MARINO BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function (jasmine) {

  if (!jasmine) {
    throw new Error("[Jasmine JSReporter] 'Jasmine' library not found");
  }

  // ------------------------------------------------------------------------
  // Jasmine JSReporter for Jasmine 1.x
  // ------------------------------------------------------------------------

  /**
   * Calculate elapsed time, in Seconds.
   * @param startMs Start time in Milliseconds
   * @param finishMs Finish time in Milliseconds
   * @return Elapsed time in Seconds */
  function elapsedSec (startMs, finishMs) {
      return (finishMs - startMs) / 1000;
  }

  /**
   * Round an amount to the given number of Digits.
   * If no number of digits is given, than '2' is assumed.
   * @param amount Amount to round
   * @param numOfDecDigits Number of Digits to round to. Default value is '2'.
   * @return Rounded amount */
  function round (amount, numOfDecDigits) {
      numOfDecDigits = numOfDecDigits || 2;
      return Math.round(amount * Math.pow(10, numOfDecDigits)) / Math.pow(10, numOfDecDigits);
  }

  /**
   * Create a new array which contains only the failed items.
   * @param items Items which will be filtered
   * @returns {Array} of failed items */
  function failures (items) {
      var fs = [], i, v;
      for (i = 0; i < items.length; i += 1) {
          v = items[i];
          if (!v.passed_) {
              fs.push(v);
          }
      }
      return fs;
  }

  /**
   * Collect information about a Suite, recursively, and return a JSON result.
   * @param suite The Jasmine Suite to get data from
   */
  function getSuiteData (suite) {
      var suiteData = {
              description : suite.description,
              durationSec : 0,
              specs: [],
              suites: [],
              passed: true
          },
          specs = suite.specs(),
          suites = suite.suites(),
          i, ilen;

      // Loop over all the Suite's Specs
      for (i = 0, ilen = specs.length; i < ilen; ++i) {
          suiteData.specs[i] = {
              description : specs[i].description,
              durationSec : specs[i].durationSec,
              passed : specs[i].results().passedCount === specs[i].results().totalCount,
              skipped : specs[i].results().skipped,
              passedCount : specs[i].results().passedCount,
              failedCount : specs[i].results().failedCount,
              totalCount : specs[i].results().totalCount,
              failures: failures(specs[i].results().getItems())
          };
          suiteData.passed = !suiteData.specs[i].passed ? false : suiteData.passed;
          suiteData.durationSec += suiteData.specs[i].durationSec;
      }

      // Loop over all the Suite's sub-Suites
      for (i = 0, ilen = suites.length; i < ilen; ++i) {
          suiteData.suites[i] = getSuiteData(suites[i]); //< recursive population
          suiteData.passed = !suiteData.suites[i].passed ? false : suiteData.passed;
          suiteData.durationSec += suiteData.suites[i].durationSec;
      }

      // Rounding duration numbers to 3 decimal digits
      suiteData.durationSec = round(suiteData.durationSec, 4);

      return suiteData;
  }

  var JSReporter =  function () {
  };

  JSReporter.prototype = {
      reportRunnerStarting: function (runner) {
          // Nothing to do
      },

      reportSpecStarting: function (spec) {
          // Start timing this spec
          spec.startedAt = new Date();
      },

      reportSpecResults: function (spec) {
          // Finish timing this spec and calculate duration/delta (in sec)
          spec.finishedAt = new Date();
          // If the spec was skipped, reportSpecStarting is never called and spec.startedAt is undefined
          spec.durationSec = spec.startedAt ? elapsedSec(spec.startedAt.getTime(), spec.finishedAt.getTime()) : 0;
      },

      reportSuiteResults: function (suite) {
          // Nothing to do
      },

      reportRunnerResults: function (runner) {
          var suites = runner.suites(),
              i, j, ilen;

          // Attach results to the "jasmine" object to make those results easy to scrap/find
          jasmine.runnerResults = {
              suites: [],
              durationSec : 0,
              passed : true
          };

          // Loop over all the Suites
          for (i = 0, ilen = suites.length, j = 0; i < ilen; ++i) {
              if (suites[i].parentSuite === null) {
                  jasmine.runnerResults.suites[j] = getSuiteData(suites[i]);
                  // If 1 suite fails, the whole runner fails
                  jasmine.runnerResults.passed = !jasmine.runnerResults.suites[j].passed ? false : jasmine.runnerResults.passed;
                  // Add up all the durations
                  jasmine.runnerResults.durationSec += jasmine.runnerResults.suites[j].durationSec;
                  j++;
              }
          }

          // Decorate the 'jasmine' object with getters
          jasmine.getJSReport = function () {
              if (jasmine.runnerResults) {
                  return jasmine.runnerResults;
              }
              return null;
          };
          jasmine.getJSReportAsString = function () {
              return JSON.stringify(jasmine.getJSReport());
          };

          // XXX: Custom Sauce
          this.reportResultsToSauceLabs(jasmine.runnerResults);
      },

      reportResultsToSauceLabs: function(results) {
          /**
           * Specs for SauceLabs reporting:
           * https://docs.saucelabs.com/reference/rest-api/#start-js-unit-tests
           */
          window.global_test_results = {
              passed: 0,
              failed: 0,
              total: 0,
              duration: results.durationSec,
              tests: []
          };

          var suites = results.suites,
              i,
              j,
              k,
              spec,
              specResult;
          for (i = 0; i < suites.length; i++) {
              for (j = 0; j < suites[i].specs.length; j++) {
                  spec = suites[i].specs[j];
                  specResult = {
                    name: spec.description,
                    message: "",
                    duration: spec.durationSec,
                    result: spec.passed
                  };
                  global_test_results.total += spec.totalCount;
                  global_test_results.passed += spec.passedCount;
                  global_test_results.failed += spec.failedCount;
                  if (spec.failures.length) {
                    for (k = 0; k < spec.failures.length; k++) {
                      specResult.message += spec.failures[k].message + "\n";
                    }
                  }
                  global_test_results.tests.push(specResult);
              }
          }
      }
  };

  // export public
  jasmine.JSReporter = JSReporter;

  // ------------------------------------------------------------------------
  // Jasmine JSReporter for Jasmine 2.0
  // ------------------------------------------------------------------------

  /*
    Simple timer implementation
  */
  var Timer = function () {};

  Timer.prototype.start = function () {
    this.startTime = new Date().getTime();
    return this;
  };

  Timer.prototype.elapsed = function () {
    if (this.startTime == null) {
      return -1;
    }
    return new Date().getTime() - this.startTime;
  };

  /*
    Utility methods
  */
  var _extend = function (obj1, obj2) {
    for (var prop in obj2) {
      obj1[prop] = obj2[prop];
    }
    return obj1;
  };
  var _clone = function (obj) {
    if (obj !== Object(obj)) {
      return obj;
    }
    return _extend({}, obj);
  };

  jasmine.JSReporter2 = function () {
    this.specs  = {};
    this.suites = {};
    this.rootSuites = [];
    this.suiteStack = [];

    // export methods under jasmine namespace
    jasmine.getJSReport = this.getJSReport;
    jasmine.getJSReportAsString = this.getJSReportAsString;
  };

  var JSR = jasmine.JSReporter2.prototype;

  // Reporter API methods
  // --------------------

  JSR.suiteStarted = function (suite) {
    suite = this._cacheSuite(suite);
    // build up suite tree as we go
    suite.specs = [];
    suite.suites = [];
    suite.passed = true;
    suite.parentId = this.suiteStack.slice(this.suiteStack.length -1)[0];
    if (suite.parentId) {
      this.suites[suite.parentId].suites.push(suite);
    } else {
      this.rootSuites.push(suite.id);
    }
    this.suiteStack.push(suite.id);
    suite.timer = new Timer().start();
  };

  JSR.suiteDone = function (suite) {
    suite = this._cacheSuite(suite);
    suite.duration = suite.timer.elapsed();
    suite.durationSec = suite.duration / 1000;
    this.suiteStack.pop();

    // maintain parent suite state
    var parent = this.suites[suite.parentId];
    if (parent) {
      parent.passed = parent.passed && suite.passed;
    }

    // keep report representation clean
    delete suite.timer;
    delete suite.id;
    delete suite.parentId;
    delete suite.fullName;
  };

  JSR.specStarted = function (spec) {
    spec = this._cacheSpec(spec);
    spec.timer = new Timer().start();
    // build up suites->spec tree as we go
    spec.suiteId = this.suiteStack.slice(this.suiteStack.length -1)[0];
    this.suites[spec.suiteId].specs.push(spec);
  };

  JSR.specDone = function (spec) {
    spec = this._cacheSpec(spec);

    spec.duration = spec.timer.elapsed();
    spec.durationSec = spec.duration / 1000;

    spec.skipped = spec.status === 'pending';
    spec.passed = spec.skipped || spec.status === 'passed';

    spec.totalCount = spec.passedExpectations.length + spec.failedExpectations.length;
    spec.passedCount = spec.passedExpectations.length;
    spec.failedCount = spec.failedExpectations.length;
    spec.failures = [];

    for (var i = 0, j = spec.failedExpectations.length; i < j; i++) {
      var fail = spec.failedExpectations[i];
      spec.failures.push({
        type: 'expect',
        expected: fail.expected,
        passed: false,
        message: fail.message,
        matcherName: fail.matcherName,
        trace: {
          stack: fail.stack
        }
      });
    }

    // maintain parent suite state
    var parent = this.suites[spec.suiteId];
    if (spec.failed) {
      parent.failingSpecs.push(spec);
    }
    parent.passed = parent.passed && spec.passed;

    // keep report representation clean
    delete spec.timer;
    delete spec.totalExpectations;
    delete spec.passedExpectations;
    delete spec.suiteId;
    delete spec.fullName;
    delete spec.id;
    delete spec.status;
    delete spec.failedExpectations;
  };

  JSR.jasmineDone = function () {
    this._buildReport();
  };

  JSR.getJSReport = function () {
    if (jasmine.jsReport) {
      return jasmine.jsReport;
    }
  };

  JSR.getJSReportAsString = function () {
    if (jasmine.jsReport) {
      return JSON.stringify(jasmine.jsReport);
    }
  };

  // Private methods
  // ---------------

  JSR._haveSpec = function (spec) {
    return this.specs[spec.id] != null;
  };

  JSR._cacheSpec = function (spec) {
    var existing = this.specs[spec.id];
    if (existing == null) {
      existing = this.specs[spec.id] = _clone(spec);
    } else {
      _extend(existing, spec);
    }
    return existing;
  };

  JSR._haveSuite = function (suite) {
    return this.suites[suite.id] != null;
  };

  JSR._cacheSuite = function (suite) {
    var existing = this.suites[suite.id];
    if (existing == null) {
      existing = this.suites[suite.id] = _clone(suite);
    } else {
      _extend(existing, suite);
    }
    return existing;
  };

  JSR._buildReport = function () {
    var overallDuration = 0;
    var overallPassed = true;
    var overallSuites = [];

    for (var i = 0, j = this.rootSuites.length; i < j; i++) {
      var suite = this.suites[this.rootSuites[i]];
      overallDuration += suite.duration;
      overallPassed = overallPassed && suite.passed;
      overallSuites.push(suite);
    }

    jasmine.jsReport = {
      passed: overallPassed,
      durationSec: overallDuration / 1000,
      suites: overallSuites
    };
  };

})(jasmine);
<commit_msg>Force run CI #64<commit_after>/*
  This file is part of the Jasmine JSReporter project from Ivan De Marino.

  Copyright (C) 2011-2014 Ivan De Marino <http://ivandemarino.me>
  Copyright (C) 2014 Alex Treppass <http://alextreppass.co.uk>

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL IVAN DE MARINO BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function (jasmine) {

  if (!jasmine) {
    throw new Error("[Jasmine JSReporter] 'Jasmine' library not found");
  }

  // ------------------------------------------------------------------------
  // Jasmine JSReporter for Jasmine 1.x
  // ------------------------------------------------------------------------

  /**
   * Calculate elapsed time, in Seconds.
   * @param startMs Start time in Milliseconds
   * @param finishMs Finish time in Milliseconds
   * @return Elapsed time in Seconds */
  function elapsedSec (startMs, finishMs) {
      return (finishMs - startMs) / 1000;
  }

  /**
   * Round an amount to the given number of Digits.
   * If no number of digits is given, than '2' is assumed.
   * @param amount Amount to round
   * @param numOfDecDigits Number of Digits to round to. Default value is '2'.
   * @return Rounded amount */
  function round (amount, numOfDecDigits) {
      numOfDecDigits = numOfDecDigits || 2;
      return Math.round(amount * Math.pow(10, numOfDecDigits)) / Math.pow(10, numOfDecDigits);
  }

  /**
   * Create a new array which contains only the failed items.
   * @param items Items which will be filtered
   * @returns {Array} of failed items */
  function failures (items) {
      var fs = [], i, v;
      for (i = 0; i < items.length; i += 1) {
          v = items[i];
          if (!v.passed_) {
              fs.push(v);
          }
      }
      return fs;
  }

  /**
   * Collect information about a Suite, recursively, and return a JSON result.
   * @param suite The Jasmine Suite to get data from
   */
  function getSuiteData (suite) {
      var suiteData = {
              description : suite.description,
              durationSec : 0,
              specs: [],
              suites: [],
              passed: true
          },
          specs = suite.specs(),
          suites = suite.suites(),
          i, ilen;

      // Loop over all the Suite's Specs
      for (i = 0, ilen = specs.length; i < ilen; ++i) {
          suiteData.specs[i] = {
              description : specs[i].description,
              durationSec : specs[i].durationSec,
              passed : specs[i].results().passedCount === specs[i].results().totalCount,
              skipped : specs[i].results().skipped,
              passedCount : specs[i].results().passedCount,
              failedCount : specs[i].results().failedCount,
              totalCount : specs[i].results().totalCount,
              failures: failures(specs[i].results().getItems())
          };
          suiteData.passed = !suiteData.specs[i].passed ? false : suiteData.passed;
          suiteData.durationSec += suiteData.specs[i].durationSec;
      }

      // Loop over all the Suite's sub-Suites
      for (i = 0, ilen = suites.length; i < ilen; ++i) {
          suiteData.suites[i] = getSuiteData(suites[i]); //< recursive population
          suiteData.passed = !suiteData.suites[i].passed ? false : suiteData.passed;
          suiteData.durationSec += suiteData.suites[i].durationSec;
      }

      // Rounding duration numbers to 3 decimal digits
      suiteData.durationSec = round(suiteData.durationSec, 4);

      return suiteData;
  }

  var JSReporter =  function () {
  };

  JSReporter.prototype = {
      reportRunnerStarting: function (runner) {
          // Nothing to do
      },

      reportSpecStarting: function (spec) {
          // Start timing this spec
          spec.startedAt = new Date();
      },

      reportSpecResults: function (spec) {
          // Finish timing this spec and calculate duration/delta (in sec)
          spec.finishedAt = new Date();
          // If the spec was skipped, reportSpecStarting is never called and spec.startedAt is undefined
          spec.durationSec = spec.startedAt ? elapsedSec(spec.startedAt.getTime(), spec.finishedAt.getTime()) : 0;
      },

      reportSuiteResults: function (suite) {
          // Nothing to do
      },

      reportRunnerResults: function (runner) {
          var suites = runner.suites(),
              i, j, ilen;

          // Attach results to the "jasmine" object to make those results easy to scrap/find
          jasmine.runnerResults = {
              suites: [],
              durationSec : 0,
              passed : true
          };

          // Loop over all the Suites
          for (i = 0, ilen = suites.length, j = 0; i < ilen; ++i) {
              if (suites[i].parentSuite === null) {
                  jasmine.runnerResults.suites[j] = getSuiteData(suites[i]);
                  // If 1 suite fails, the whole runner fails
                  jasmine.runnerResults.passed = !jasmine.runnerResults.suites[j].passed ? false : jasmine.runnerResults.passed;
                  // Add up all the durations
                  jasmine.runnerResults.durationSec += jasmine.runnerResults.suites[j].durationSec;
                  j++;
              }
          }

          // Decorate the 'jasmine' object with getters
          jasmine.getJSReport = function () {
              if (jasmine.runnerResults) {
                  return jasmine.runnerResults;
              }
              return null;
          };
          jasmine.getJSReportAsString = function () {
              return JSON.stringify(jasmine.getJSReport());
          };

          // XXX: Custom Sauce
          this.reportResultsToSauceLabs(jasmine.runnerResults);
      },

      reportResultsToSauceLabs: function(results) {
          /**
           * Specs for SauceLabs reporting:
           * https://docs.saucelabs.com/reference/rest-api/#start-js-unit-tests
           */
          window.global_test_results = {
              passed: 0,
              failed: 0,
              total: 0,
              duration: results.durationSec,
              tests: []
          };

          var suites = results.suites,
              i,
              j,
              k,
              spec,
              specResult;
          for (i = 0; i < suites.length; i++) {
              for (j = 0; j < suites[i].specs.length; j++) {
                  spec = suites[i].specs[j];
                  specResult = {
                    name: spec.description,
                    message: "",
                    duration: spec.durationSec,
                    result: spec.passed
                  };

                  global_test_results.total += spec.totalCount;
                  global_test_results.passed += spec.passedCount;
                  global_test_results.failed += spec.failedCount;
                  if (spec.failures.length) {
                    for (k = 0; k < spec.failures.length; k++) {
                      specResult.message += spec.failures[k].message + "\n";
                    }
                  }
                  global_test_results.tests.push(specResult);
              }
          }
      }
  };

  // export public
  jasmine.JSReporter = JSReporter;

  // ------------------------------------------------------------------------
  // Jasmine JSReporter for Jasmine 2.0
  // ------------------------------------------------------------------------

  /*
    Simple timer implementation
  */
  var Timer = function () {};

  Timer.prototype.start = function () {
    this.startTime = new Date().getTime();
    return this;
  };

  Timer.prototype.elapsed = function () {
    if (this.startTime == null) {
      return -1;
    }
    return new Date().getTime() - this.startTime;
  };

  /*
    Utility methods
  */
  var _extend = function (obj1, obj2) {
    for (var prop in obj2) {
      obj1[prop] = obj2[prop];
    }
    return obj1;
  };
  var _clone = function (obj) {
    if (obj !== Object(obj)) {
      return obj;
    }
    return _extend({}, obj);
  };

  jasmine.JSReporter2 = function () {
    this.specs  = {};
    this.suites = {};
    this.rootSuites = [];
    this.suiteStack = [];

    // export methods under jasmine namespace
    jasmine.getJSReport = this.getJSReport;
    jasmine.getJSReportAsString = this.getJSReportAsString;
  };

  var JSR = jasmine.JSReporter2.prototype;

  // Reporter API methods
  // --------------------

  JSR.suiteStarted = function (suite) {
    suite = this._cacheSuite(suite);
    // build up suite tree as we go
    suite.specs = [];
    suite.suites = [];
    suite.passed = true;
    suite.parentId = this.suiteStack.slice(this.suiteStack.length -1)[0];
    if (suite.parentId) {
      this.suites[suite.parentId].suites.push(suite);
    } else {
      this.rootSuites.push(suite.id);
    }
    this.suiteStack.push(suite.id);
    suite.timer = new Timer().start();
  };

  JSR.suiteDone = function (suite) {
    suite = this._cacheSuite(suite);
    suite.duration = suite.timer.elapsed();
    suite.durationSec = suite.duration / 1000;
    this.suiteStack.pop();

    // maintain parent suite state
    var parent = this.suites[suite.parentId];
    if (parent) {
      parent.passed = parent.passed && suite.passed;
    }

    // keep report representation clean
    delete suite.timer;
    delete suite.id;
    delete suite.parentId;
    delete suite.fullName;
  };

  JSR.specStarted = function (spec) {
    spec = this._cacheSpec(spec);
    spec.timer = new Timer().start();
    // build up suites->spec tree as we go
    spec.suiteId = this.suiteStack.slice(this.suiteStack.length -1)[0];
    this.suites[spec.suiteId].specs.push(spec);
  };

  JSR.specDone = function (spec) {
    spec = this._cacheSpec(spec);

    spec.duration = spec.timer.elapsed();
    spec.durationSec = spec.duration / 1000;

    spec.skipped = spec.status === 'pending';
    spec.passed = spec.skipped || spec.status === 'passed';

    spec.totalCount = spec.passedExpectations.length + spec.failedExpectations.length;
    spec.passedCount = spec.passedExpectations.length;
    spec.failedCount = spec.failedExpectations.length;
    spec.failures = [];

    for (var i = 0, j = spec.failedExpectations.length; i < j; i++) {
      var fail = spec.failedExpectations[i];
      spec.failures.push({
        type: 'expect',
        expected: fail.expected,
        passed: false,
        message: fail.message,
        matcherName: fail.matcherName,
        trace: {
          stack: fail.stack
        }
      });
    }

    // maintain parent suite state
    var parent = this.suites[spec.suiteId];
    if (spec.failed) {
      parent.failingSpecs.push(spec);
    }
    parent.passed = parent.passed && spec.passed;

    // keep report representation clean
    delete spec.timer;
    delete spec.totalExpectations;
    delete spec.passedExpectations;
    delete spec.suiteId;
    delete spec.fullName;
    delete spec.id;
    delete spec.status;
    delete spec.failedExpectations;
  };

  JSR.jasmineDone = function () {
    this._buildReport();
  };

  JSR.getJSReport = function () {
    if (jasmine.jsReport) {
      return jasmine.jsReport;
    }
  };

  JSR.getJSReportAsString = function () {
    if (jasmine.jsReport) {
      return JSON.stringify(jasmine.jsReport);
    }
  };

  // Private methods
  // ---------------

  JSR._haveSpec = function (spec) {
    return this.specs[spec.id] != null;
  };

  JSR._cacheSpec = function (spec) {
    var existing = this.specs[spec.id];
    if (existing == null) {
      existing = this.specs[spec.id] = _clone(spec);
    } else {
      _extend(existing, spec);
    }
    return existing;
  };

  JSR._haveSuite = function (suite) {
    return this.suites[suite.id] != null;
  };

  JSR._cacheSuite = function (suite) {
    var existing = this.suites[suite.id];
    if (existing == null) {
      existing = this.suites[suite.id] = _clone(suite);
    } else {
      _extend(existing, suite);
    }
    return existing;
  };

  JSR._buildReport = function () {
    var overallDuration = 0;
    var overallPassed = true;
    var overallSuites = [];

    for (var i = 0, j = this.rootSuites.length; i < j; i++) {
      var suite = this.suites[this.rootSuites[i]];
      overallDuration += suite.duration;
      overallPassed = overallPassed && suite.passed;
      overallSuites.push(suite);
    }

    jasmine.jsReport = {
      passed: overallPassed,
      durationSec: overallDuration / 1000,
      suites: overallSuites
    };
  };

})(jasmine);
<|endoftext|>


<commit_before>// jscs:disable requireMultipleVarDecl
// jscs:disable requireVarDeclFirst
/*global atom require module setTimeout*/

module.exports = {
    activate: function () { // state parameter not needed
        "use strict";
        var path = require("path");
        var fs = require("fs");
        var profileHome = path.join(atom.packages.getPackageDirPaths()[0], "..");
        var configFile = path.join(profileHome, "config.cson");

        var readStream = fs.createReadStream(configFile);
        readStream.on("error", function (err) {
            throw (err);
        });

        var writeStream = fs.createWriteStream(configFile + ".bak");
        readStream.on("error", function (err) {
            throw (err);
        });

        readStream.pipe(writeStream);

        atom.workspace.onDidChangeActivePaneItem(function () {
            return module.exports.applyFix();
        });

        // Call hack on start also
        module.exports.applyFix();
    },
    deactivate: function () {
        "use strict";
    },
    applyFix: function () {
        "use strict";
    }
};
<commit_msg>Alignment fix now uses new async API<commit_after>// jscs:disable requireMultipleVarDecl
// jscs:disable requireVarDeclFirst
/*global atom require module setTimeout*/

module.exports = {
    // state parameter not needed
    activate: function() {
        "use strict";
        var path = require("path");
        var fs = require("fs");
        var profileHome = path.join( atom.packages.getPackageDirPaths()[ 0 ], "..");
        var configFile = path.join( profileHome, "config.cson");

        var readStream = fs.createReadStream( configFile );
        readStream.on("error", function( err ) {
            throw (err);
        });

        var writeStream = fs.createWriteStream( configFile + ".bak");
        readStream.on("error", function( err ) {
            throw (err);
        });

        readStream.pipe( writeStream );

        atom.workspace.onDidStopChangingActivePaneItem(function( item ) {
            if ( item !== undefined && item !== null ) {
                module.exports.applyFix();
            }
        });

    },
    deactivate: function() {
        "use strict";
    },
    applyFix: function( textEditor ) {
        "use strict";
        atom.workspace.decreaseFontSize();
        atom.workspace.increaseFontSize();
    }
};
<|endoftext|>


<commit_before>'use strict';

/**
 * Module dependencies.
 */
var express = require('express'),
    mongoStore = require('connect-mongo')(express),
    flash = require('connect-flash'),
    helpers = require('view-helpers'),
    config = require('./config');

module.exports = function(app, passport, db) {
    app.set('showStackError', true);

    // Prettify HTML
    app.locals.pretty = true;

    // Should be placed before express.static
    // To ensure that all assets and data are compressed (utilize bandwidth)
    app.use(express.compress({
        filter: function(req, res) {
            return (/json|text|javascript|css/).test(res.getHeader('Content-Type'));
        },
        level: 9
    }));

    // Don't use logger for test env
    if (process.env.NODE_ENV !== 'test') {
        app.use(express.logger('dev'));
    }

    // Set views path, template engine and default layout
    app.set('views', config.root + '/app/views');
    app.set('view engine', 'jade');

    // Enable jsonp
    app.enable("jsonp callback");

    app.configure(function() {
        // The cookieParser should be above session
        app.use(express.cookieParser());

        // Request body parsing middleware should be above methodOverride
        app.use(express.urlencoded());
        app.use(express.json());
        app.use(express.methodOverride());

        // Express/Mongo session storage
        app.use(express.session({
            secret: 'MEAN',
            store: new mongoStore({
                db: db.connection.db,
                collection: 'sessions'
            })
        }));

        // Connect flash for flash messages
        app.use(flash());

        // Dynamic helpers
        app.use(helpers(config.app.name));

        // Use passport session
        app.use(passport.initialize());
        app.use(passport.session());

        // Routes should be at the last
        app.use(app.router);

        // Setting the fav icon and static folder
        app.use(express.favicon());
        app.use(express.static(config.root + '/public'));

        // Assume "not found" in the error msgs is a 404. this is somewhat
        // silly, but valid, you can do whatever you like, set properties,
        // use instanceof etc.
        app.use(function(err, req, res, next) {
            // Treat as 404
            if (~err.message.indexOf('not found')) return next();

            // Log it
            console.error(err.stack);

            // Error page
            res.status(500).render('500', {
                error: err.stack
            });
        });

        // Assume 404 since no middleware responded
        app.use(function(req, res, next) {
            res.status(404).render('404', {
                url: req.originalUrl,
                error: 'Not found'
            });
        });

    });
};<commit_msg>updating documentation<commit_after>'use strict';

/**
 * Module dependencies.
 */
var express = require('express'),
    mongoStore = require('connect-mongo')(express),
    flash = require('connect-flash'),
    helpers = require('view-helpers'),
    config = require('./config');

module.exports = function(app, passport, db) {
    app.set('showStackError', true);

    // Prettify HTML
    app.locals.pretty = true;

    // Should be placed before express.static
    // To ensure that all assets and data are compressed (utilize bandwidth)
    app.use(express.compress({
        filter: function(req, res) {
            return (/json|text|javascript|css/).test(res.getHeader('Content-Type'));
        },
        // Levels are specified in a range of 0 to 9, where-as 0 is
        // no compression and 9 is best compression, but slowest
        level: 9
    }));

    // Don't use logger for test env
    if (process.env.NODE_ENV !== 'test') {
        app.use(express.logger('dev'));
    }

    // Set views path, template engine and default layout
    app.set('views', config.root + '/app/views');
    app.set('view engine', 'jade');

    // Enable jsonp
    app.enable("jsonp callback");

    app.configure(function() {
        // The cookieParser should be above session
        app.use(express.cookieParser());

        // Request body parsing middleware should be above methodOverride
        app.use(express.urlencoded());
        app.use(express.json());
        app.use(express.methodOverride());

        // Express/Mongo session storage
        app.use(express.session({
            secret: 'MEAN',
            store: new mongoStore({
                db: db.connection.db,
                collection: 'sessions'
            })
        }));

        // Connect flash for flash messages
        app.use(flash());

        // Dynamic helpers
        app.use(helpers(config.app.name));

        // Use passport session
        app.use(passport.initialize());
        app.use(passport.session());

        // Routes should be at the last
        app.use(app.router);

        // Setting the fav icon and static folder
        app.use(express.favicon());
        app.use(express.static(config.root + '/public'));

        // Assume "not found" in the error msgs is a 404. this is somewhat
        // silly, but valid, you can do whatever you like, set properties,
        // use instanceof etc.
        app.use(function(err, req, res, next) {
            // Treat as 404
            if (~err.message.indexOf('not found')) return next();

            // Log it
            console.error(err.stack);

            // Error page
            res.status(500).render('500', {
                error: err.stack
            });
        });

        // Assume 404 since no middleware responded
        app.use(function(req, res, next) {
            res.status(404).render('404', {
                url: req.originalUrl,
                error: 'Not found'
            });
        });

    });
};<|endoftext|>


<commit_before>'use strict';

const express = require('express');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const session = require('express-session');
const passport = require('passport');
const morgan = require('morgan');
const logger = require('./logger');

module.exports = function (config) {
  const app = express();

  logger.debug("Overriding 'Express' logger");
  app.use(morgan('dev', {
    'stream': logger.stream
  }));

  logger.debug("Setting 'Pug' as view engine");
  app.set('view engine', 'pug');

  logger.debug("Setting 'Views' folder");
  const viewsFolder = config.rootPath + 'lib/views';
  app.set('views', viewsFolder);

  logger.debug("Setting 'Cookie Parser' as ");
  app.use(cookieParser());

  logger.debug('Setting parse urlencoded request bodies into req.body.');
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({
    extended: true
  }));

  logger.debug('Setting secret settings');
  app.use(session({
    secret: config.sessionSecret,
    resave: true,
    saveUninitialized: true
  }));

  logger.debug("Setting 'Passport' as login engine");
  app.use(passport.initialize());
  app.use(passport.session());
  app.use((req, res, next) => {
    if (req.user) {
      res.locals.currentUser = req.user;
    }
    next();
  });

  logger.debug("Setting 'Public' folder");
  app.use(express.static(config.rootPath + 'public'));

  return app;
};<commit_msg>small typos<commit_after>'use strict';

const express = require('express');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const session = require('express-session');
const passport = require('passport');
const morgan = require('morgan');
const logger = require('./logger');

module.exports = function (config) {
  const app = express();

  logger.debug('Overriding "Express" logger');
  app.use(morgan('dev', {
    'stream': logger.stream
  }));

  logger.debug('Setting "Pug" as view engine');
  app.set('view engine', 'pug');

  logger.debug('Setting "Views" folder');
  const viewsFolder = config.rootPath + 'lib/views';
  app.set('views', viewsFolder);

  logger.debug('Setting "Cookie Parser" as ');
  app.use(cookieParser());

  logger.debug('Setting parse urlencoded request bodies into req.body.');
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({
    extended: true
  }));

  logger.debug('Setting secret settings');
  app.use(session({
    secret: config.sessionSecret,
    resave: true,
    saveUninitialized: true
  }));

  logger.debug('Setting "Passport" as login engine');
  app.use(passport.initialize());
  app.use(passport.session());
  app.use((req, res, next) => {
    if (req.user) {
      res.locals.currentUser = req.user;
    }
    next();
  });

  logger.debug('Setting "Public" folder');
  app.use(express.static(config.rootPath + 'public'));

  return app;
};<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var express = require('express');
var session = require('express-session');
var compression = require('compression');
var morgan = require('morgan');
var cookieParser = require('cookie-parser');
var cookieSession = require('cookie-session');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var csrf = require('csurf');
var multer = require('multer');
var swig = require('swig');

var mongoStore = require('connect-mongo')(session);
var flash = require('connect-flash');
var winston = require('winston');
var helpers = require('view-helpers');
var config = require('../config/config');
var pkg = require('../package.json');

var env = process.env.NODE_ENV || 'development';

/**
 * Expose
 */

module.exports = function (app, passport) {

  // Compression middleware (should be placed before express.static)
  app.use(compression({
    threshold: 512
  }));

  // Static files middleware
  app.use(express.static(config.root + '/public'));

  // Use winston on production
  var log;
  if (env !== 'development') {
    log = {
      stream: {
        write: function (message, encoding) {
          winston.info(message);
        }
      }
    };
  } else {
    log = 'dev';
  }

  // Don't log during tests
  // Logging middleware
  if (env !== 'test') app.use(morgan(log));

  // Swig templating engine settings
  if (env === 'development' || env === 'test') {
    swig.setDefaults({
      cache: false
    });
  }

  // set views path, template engine and default layout
  app.engine('html', swig.renderFile);
  app.set('views', './app/views/');
  app.set('view engine', 'html');

  // expose package.json to views
  app.use(function (req, res, next) {
    res.locals.pkg = pkg;
    res.locals.env = env;
    next();
  });

  // bodyParser should be above methodOverride
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(multer());
  app.use(methodOverride(function (req, res) {
    if (req.body && typeof req.body === 'object' && '_method' in req.body) {
      // look in urlencoded POST bodies and delete it
      var method = req.body._method;
      delete req.body._method;
      return method;
    }
  }));

  // CookieParser should be above session
  app.use(cookieParser());
  app.use(cookieSession({ secret: 'secret' }));
  app.use(session({
    resave: true,
    saveUninitialized: true,
    secret: pkg.name,
    store: new mongoStore({
      url: config.db,
      collection : 'sessions'
    })
  }));

  // use passport session
  app.use(passport.initialize());
  app.use(passport.session());

  // connect flash for flash messages - should be declared after sessions
  app.use(flash());

  // should be declared after session and flash
  app.use(helpers(pkg.name));

  // adds CSRF support
  if (process.env.NODE_ENV !== 'test') {
    app.use(csrf());

    // This could be moved to view-helpers :-)
    app.use(function (req, res, next) {
      res.locals.csrf_token = req.csrfToken();
      next();
    });
  }
};
<commit_msg>changing' back config.<commit_after>
/**
 * Module dependencies.
 */

var express = require('express');
var session = require('express-session');
var compression = require('compression');
var morgan = require('morgan');
var cookieParser = require('cookie-parser');
var cookieSession = require('cookie-session');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var csrf = require('csurf');
var multer = require('multer');
var swig = require('swig');

var mongoStore = require('connect-mongo')(session);
var flash = require('connect-flash');
var winston = require('winston');
var helpers = require('view-helpers');
var config = require('../config/config');
var pkg = require('../package.json');

var env = process.env.NODE_ENV || 'development';

/**
 * Expose
 */

module.exports = function (app, passport) {

  // Compression middleware (should be placed before express.static)
  app.use(compression({
    threshold: 512
  }));

  // Static files middleware
  app.use(express.static(config.root + '/public'));

  // Use winston on production
  var log;
  if (env !== 'development') {
    log = {
      stream: {
        write: function (message, encoding) {
          winston.info(message);
        }
      }
    };
  } else {
    log = 'dev';
  }

  // Don't log during tests
  // Logging middleware
  if (env !== 'test') app.use(morgan(log));

  // Swig templating engine settings
  if (env === 'development' || env === 'test') {
    swig.setDefaults({
      cache: false
    });
  }

  // set views path, template engine and default layout
  app.engine('html', swig.renderFile);
  app.set('views', config.root + '/app/views');
  app.set('view engine', 'html');

  // expose package.json to views
  app.use(function (req, res, next) {
    res.locals.pkg = pkg;
    res.locals.env = env;
    next();
  });

  // bodyParser should be above methodOverride
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(multer());
  app.use(methodOverride(function (req, res) {
    if (req.body && typeof req.body === 'object' && '_method' in req.body) {
      // look in urlencoded POST bodies and delete it
      var method = req.body._method;
      delete req.body._method;
      return method;
    }
  }));

  // CookieParser should be above session
  app.use(cookieParser());
  app.use(cookieSession({ secret: 'secret' }));
  app.use(session({
    resave: true,
    saveUninitialized: true,
    secret: pkg.name,
    store: new mongoStore({
      url: config.db,
      collection : 'sessions'
    })
  }));

  // use passport session
  app.use(passport.initialize());
  app.use(passport.session());

  // connect flash for flash messages - should be declared after sessions
  app.use(flash());

  // should be declared after session and flash
  app.use(helpers(pkg.name));

  // adds CSRF support
  if (process.env.NODE_ENV !== 'test') {
    app.use(csrf());

    // This could be moved to view-helpers :-)
    app.use(function (req, res, next) {
      res.locals.csrf_token = req.csrfToken();
      next();
    });
  }
};
<|endoftext|>


<commit_before>/**
 * Shopware 4.0
 * Copyright © 2012 shopware AG
 *
 * According to our dual licensing model, this program can be used either
 * under the terms of the GNU Affero General Public License, version 3,
 * or under a proprietary license.
 *
 * The texts of the GNU Affero General Public License with an additional
 * permission and of our proprietary license can be found at and
 * in the LICENSE file you have received along with this program.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * "Shopware" is a registered trademark of shopware AG.
 * The licensing of the program under the AGPLv3 does not imply a
 * trademark license. Therefore any rights, title and interest in
 * our trademarks remain entirely with us.
 *
 * @category   Shopware
 * @package    NewsletterManager
 * @subpackage Controller
 * @copyright  Copyright (c) 2012, shopware AG (http://www.shopware.de)
 * @version    $Id$
 * @author shopware AG
 */

//{namespace name=backend/newsletter_manager/main}

/**
 * Shopware Controller - Overview controller
 * For events and actions fired in the overview tab
 */
//{block name="backend/newsletter_manager/controller/editor"}
Ext.define('Shopware.apps.NewsletterManager.controller.Editor', {

    extend: 'Ext.app.Controller',

    snippets: {
        saveNewsletter: {
            successTitle: '{s name=saveNewsletter/successTitle}Successfully saved{/s}',
            successMessage: '{s name=saveNewsletter/successMessage}Successfully saved the newsletter{/s}',
            errorTitle: '{s name=saveNewsletter/errorTitle}Error{/s}',
            errorMessage: '{s name=saveNewsletter/errorMessage}An error occured while saving the newsletter{/s}'
        },
        testNewsletter: {
            successTitle: '{s name=testNewsletter/successTitle}Successfully sended{/s}',
            successMessage: '{s name=testNewsletter/successMessage}Successfully mailed the newsletter{/s}',
            errorTitle: '{s name=testNewsletter/errorTitle}Error{/s}',
            errorMessage: '{s name=testNewsletter/errorMessage}An error occured while sending the newsletter{/s}'
        },
    growl: '{s name=title}Newsletter Manager{/s}'
    },

    refs:[
        { ref:'newsletterEditor', selector:'newsletter-manager-newsletter-editor' },
        { ref:'newsletterSettings', selector:'newsletter-manager-newsletter-settings' },
        { ref:'newsletterWindow', selector:'newsletter-manager-newsletter-window' }
    ],

    /**
     * A template method that is called when your application boots. It is called before the Application's
     * launch function is executed so gives a hook point to run any code before your Viewport is created.
     */
    init: function() {
        var me = this;

        me.control(                {
            'newsletter-manager-newsletter-editor': {
                'sendTestMail': me.onSendTestMail,
                'openPreview': me.onOpenPreview
            },
            'newsletter-manager-newsletter-settings': {
                'formChanged': me.onFormChanged
            },
            'newsletter-manager-bottom-toolbar': {
                'backToOverview': me.onBackToOverview,
                'saveMail': me.onSaveMail
            }
        });

        me.callParent(arguments);
    },

    /**
     * Called when the user clicked the 'back to overview' button
     */
    onBackToOverview: function() {
        var me = this,
            window = me.getNewsletterWindow();

        window.destroy();

    },

    /**
     * Called when a field in the settings form changes
     * @param field
     */
    onFormChanged: function(form) {
        var me = this,
            window = me.getNewsletterWindow();

        if(form.isValid()){
            window.toolbar.saveButton.enable();
            return;
        }
        window.toolbar.saveButton.disable();
    },

    /**
     * Called when the user clicks the 'save' button in the settings view
     */
    onSaveMail: function() {
        var me = this,
              form = me.getNewsletterSettings().form,
              newsletter = me.createNewsletterModel(me.getNewsletterEditor().record);

        // Only save if the form is valid. As formBind: true only works for buttons on the form
        // and as we want to have buttons in the editor as well as in the settings (sw-3195)
        // we use a msgbox here
        if(!form.isValid()) {
            Ext.Msg.show({
                title: '{s name=fillForm/title}Required fields missing{/s}',
                msg: '{s name=fillForm/msg}You need to set all required field in the settings first{/s}',
                buttons: Ext.Msg.OK
            });
            return;
        }

          // persist
          newsletter.save({
              callback: function(data, operation){
                  var records = operation.getRecords(),
                      record = records[0],
                      rawData = record.getProxy().getReader().rawData;

                  if(operation.success){
                      Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.successTitle, me.snippets.saveNewsletter.successMessage, me.snippets.growl);
                      me.subApplication.mailingStore.reload();

                  }else{
                      Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage, me.snippets.growl);
                  }
              }
          });
    },

    /**
     * Called when the user clicked the 'send test mail' button
     */
    onSendTestMail: function() {
        var me = this,
            address = me.getNewsletterEditor().down('textfield').getValue(),
            newsletter = me.createNewsletterModel();

        newsletter.set('status', -1); // mark the newsletter as preview
        newsletter.set('id', null); // don't overwrite existing newsletters

        // The mail-method of the pre-existing newsletter controller reads the newsletters from db.
        // In order not to add additional overhead, simply write the newsletter to db and clean it up next time
        newsletter.save({
            callback: function(data, operation){
                var records = operation.getRecords(),
                    record = records[0],
                    rawData = record.getProxy().getReader().rawData;
                if(operation.success){
                    // do the actual ajax query to send the mail
                    Ext.Ajax.request({
                        url: '{url controller=Newsletter action="mail"}',
                        method: 'POST',
                        params: {
                            id: newsletter.get('id'),
                            testmail: address
                        },
                        success: function(response) {
                            Shopware.Notification.createGrowlMessage(me.snippets.testNewsletter.successTitle, me.snippets.testNewsletter.successMessage, me.snippets.growl);
                        },
                        failure: function(response) {
                            Shopware.Notification.createGrowlMessage(me.snippets.testNewsletter.errorTitle, me.snippets.testNewsletter.errorMessage, me.snippets.growl);
                        }
                    });
                }else{
                    Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage, me.snippets.growl);
                }
            }
        });
    },

    /**
     * A simple helper function which reads out the settings from the newsletters window.
     * It it used, as the mailing-model cannot be easily written into the settings form as some field-types do not match
     * e.g. dispatch/plaintext or groups
     */
    getSettings: function() {
        var me = this,
            record,
            count, totalCount = 0,
            editor = me.getNewsletterEditor(),
            settingsForm = me.getNewsletterSettings(),
            newsletterGroups = settingsForm.newsletterGroups, customerGroups = settingsForm.customerGroups,
            groups = Ext.create('Shopware.apps.NewsletterManager.store.RecipientGroup'),
            content = editor.tinyMce.getEditor().getContent();

        // Iterate the checkboxes and populate the RecipientGroupStore with checked customer groups
        Ext.each(customerGroups, function(checkbox) {
            var record = checkbox.record,
                count = checkbox.count,
                value = checkbox.getValue();

            //todo@dn: set count to the number of users in the given group
            if(value === true) {
                totalCount += count;

                record = Ext.create('Shopware.apps.NewsletterManager.model.RecipientGroup', {
                    internalId: record.get('id'),
                    number: count,
                    name: record.get('name'),
                    groupkey: record.get('key'),
                    isCustomerGroup: true
                });
                groups.add(record);
            }
        });

        // Iterate the checkboxes and populate the RecipientGroupStore with checked newsletter groups
        Ext.each(newsletterGroups, function(checkbox) {
            var record = checkbox.record,
                    count = checkbox.count,
                value = checkbox.getValue();

            //todo@dn: set count to the number of users in the given group
            if(value === true) {
                totalCount += count;

                record = Ext.create('Shopware.apps.NewsletterManager.model.RecipientGroup', {
                    internalId: record.get('id'),
                    number: count,
                    name: record.get('name'),
                    groupkey: false,
                    isCustomerGroup: false
                });
                groups.add(record);
            }

        });

        var settings = Ext.create('Shopware.apps.NewsletterManager.model.Settings'),
            values = settingsForm.getValues();

        // Copy the values from the form into the settings model
        settings.set(values);

        if(settingsForm.record.get('id') !== null){
            settings.set('id', settingsForm.record.get('id'));
        }
        settings.set('content', content);
        settings.set('groups', groups);
        settings.set('recipients', totalCount);
        if(values['dispatch'] == 1){
            settings.set('plaintext', false);
        }else{
            settings.set('plaintext', true);
        }
        var senderRecord = me.subApplication.senderStore.getById(values['senderId']);
        settings.set('senderName', senderRecord.get('name'));
        settings.set('senderMail', senderRecord.get('email'));

        return settings;

    },

    /**
     * Helper function that creates and returns a newsletter model from the current fields
     * @param record, optional. If given, the given record will be updated instead of creating a new
     */
    createNewsletterModel: function(record) {
        var me = this,
            newsletter, container, ctText, config,
            textStore, containerStore,
            settings = me.getSettings();

        // Create model and store for the text-field (content)
        ctText = Ext.create('Shopware.apps.NewsletterManager.model.ContainerTypeText', {
            headline: settings.get('subject'),
            content:settings.get('content'),
            image: '',
            link: '',
            alignment: 'left'
        });
        textStore = Ext.create('Ext.data.Store', { model: 'Shopware.apps.NewsletterManager.model.ContainerTypeText' });
        textStore.add(ctText);

        // Create model and store for the containers.
        container = Ext.create('Shopware.apps.NewsletterManager.model.Container', {
            value: '',
            type: 'ctText',
            description: '',
            position: 1
        });
        container.getTextStore = textStore;
        containerStore = Ext.create('Ext.data.Store', { model: 'Shopware.apps.NewsletterManager.model.Container' });
        containerStore.add(container);

        config =  {
            subject: settings.get('subject'),
            customerGroup: settings.get('customerGroup'),
            recipients: settings.get('recipients'),
            plaintext: settings.get('plaintext'),
            senderName: settings.get('senderName'),
            senderMail: settings.get('senderMail'),
            groups: settings.get('groups'),
            languageId: settings.get('languageId'),

            date :  null,
            locked: null

        };

        // Create model for the newsletter
        if(record == null) {
            newsletter = Ext.create('Shopware.apps.NewsletterManager.model.Mailing', config);
        }else{
            record.set(config);
            newsletter = record;
        }

        if(settings.get('id') !== null){
            newsletter.set('id', settings.get('id'));
        }

        newsletter.getContainersStore = containerStore;
        newsletter.getGroupsStore = settings.get('groups');

        return newsletter;
    },

    /**
     * Called after the user clicked the 'create preview' button in the newsletter window
     * @param tinyMCE
     */
    onOpenPreview: function(tinyMCE) {
        var me = this,
            newsletter = me.createNewsletterModel();

        newsletter.set('status', -1); // mark the newsletter as preview
        newsletter.set('id', null); // don't overwrite existing newsletters

        // The mail-method of the pre-existing newsletter controller reads the newsletters from db.
        // In order not to add additional overhead, simply write the newsletter to db and clean it up next time
        newsletter.save({
            callback: function(data, operation){
                var records = operation.getRecords(),
                    record = records[0],
                    rawData = record.getProxy().getReader().rawData;
                if(operation.success){
                    if(!Ext.isEmpty(rawData.data)){
                        // Open up a iframe and show the preview
                        var pos = location.href.search("/backend");
                        var url = location.href.substr(0, pos) + "/backend/Newsletter/view?id=" + rawData.data.id;

                        new Ext.Window({
                            title : '{s name=preview}Preview: {/s}' + newsletter.get('subject'),
                            width : 600,
                            height: 480,
                            layout : 'fit',
                            items : [{
                                xtype : "component",
                                id    : 'iframe-win',  // Add id
                                autoEl : {
                                    tag : "iframe",
                                    src : url
                                }
                            }]
                        }).show();

                    }

                }else{
                    Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage, me.snippets.growl);
                }
            }
        });

    }

});
//{/block}<commit_msg>NTR - Adds concrete error messages if saving a newsletter fails<commit_after>/**
 * Shopware 4.0
 * Copyright © 2012 shopware AG
 *
 * According to our dual licensing model, this program can be used either
 * under the terms of the GNU Affero General Public License, version 3,
 * or under a proprietary license.
 *
 * The texts of the GNU Affero General Public License with an additional
 * permission and of our proprietary license can be found at and
 * in the LICENSE file you have received along with this program.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * "Shopware" is a registered trademark of shopware AG.
 * The licensing of the program under the AGPLv3 does not imply a
 * trademark license. Therefore any rights, title and interest in
 * our trademarks remain entirely with us.
 *
 * @category   Shopware
 * @package    NewsletterManager
 * @subpackage Controller
 * @copyright  Copyright (c) 2012, shopware AG (http://www.shopware.de)
 * @version    $Id$
 * @author shopware AG
 */

//{namespace name=backend/newsletter_manager/main}

/**
 * Shopware Controller - Overview controller
 * For events and actions fired in the overview tab
 */
//{block name="backend/newsletter_manager/controller/editor"}
Ext.define('Shopware.apps.NewsletterManager.controller.Editor', {

    extend: 'Ext.app.Controller',

    snippets: {
        saveNewsletter: {
            successTitle: '{s name=saveNewsletter/successTitle}Successfully saved{/s}',
            successMessage: '{s name=saveNewsletter/successMessage}Successfully saved the newsletter{/s}',
            errorTitle: '{s name=saveNewsletter/errorTitle}Error{/s}',
            errorMessage: '{s name=saveNewsletter/errorMessage}An error occured while saving the newsletter{/s}'
        },
        testNewsletter: {
            successTitle: '{s name=testNewsletter/successTitle}Successfully sended{/s}',
            successMessage: '{s name=testNewsletter/successMessage}Successfully mailed the newsletter{/s}',
            errorTitle: '{s name=testNewsletter/errorTitle}Error{/s}',
            errorMessage: '{s name=testNewsletter/errorMessage}An error occured while sending the newsletter{/s}'
        },
    growl: '{s name=title}Newsletter Manager{/s}'
    },

    refs:[
        { ref:'newsletterEditor', selector:'newsletter-manager-newsletter-editor' },
        { ref:'newsletterSettings', selector:'newsletter-manager-newsletter-settings' },
        { ref:'newsletterWindow', selector:'newsletter-manager-newsletter-window' }
    ],

    /**
     * A template method that is called when your application boots. It is called before the Application's
     * launch function is executed so gives a hook point to run any code before your Viewport is created.
     */
    init: function() {
        var me = this;

        me.control(                {
            'newsletter-manager-newsletter-editor': {
                'sendTestMail': me.onSendTestMail,
                'openPreview': me.onOpenPreview
            },
            'newsletter-manager-newsletter-settings': {
                'formChanged': me.onFormChanged
            },
            'newsletter-manager-bottom-toolbar': {
                'backToOverview': me.onBackToOverview,
                'saveMail': me.onSaveMail
            }
        });

        me.callParent(arguments);
    },

    /**
     * Called when the user clicked the 'back to overview' button
     */
    onBackToOverview: function() {
        var me = this,
            window = me.getNewsletterWindow();

        window.destroy();

    },

    /**
     * Called when a field in the settings form changes
     * @param field
     */
    onFormChanged: function(form) {
        var me = this,
            window = me.getNewsletterWindow();

        if(form.isValid()){
            window.toolbar.saveButton.enable();
            return;
        }
        window.toolbar.saveButton.disable();
    },

    /**
     * Called when the user clicks the 'save' button in the settings view
     */
    onSaveMail: function() {
        var me = this,
              form = me.getNewsletterSettings().form,
              newsletter = me.createNewsletterModel(me.getNewsletterEditor().record);

        // Only save if the form is valid. As formBind: true only works for buttons on the form
        // and as we want to have buttons in the editor as well as in the settings (sw-3195)
        // we use a msgbox here
        if(!form.isValid()) {
            Ext.Msg.show({
                title: '{s name=fillForm/title}Required fields missing{/s}',
                msg: '{s name=fillForm/msg}You need to set all required field in the settings first{/s}',
                buttons: Ext.Msg.OK
            });
            return;
        }

          // persist
          newsletter.save({
              callback: function(data, operation){
                  var records = operation.getRecords(),
                      record = records[0],
                      rawData = record.getProxy().getReader().rawData;

                  if(operation.success){
                      Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.successTitle, me.snippets.saveNewsletter.successMessage, me.snippets.growl);
                      me.subApplication.mailingStore.reload();

                  }else{
                      if(rawData && rawData.data) {
                          Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage + "\r\n<br />" + rawData.data, me.snippets.growl);
                          return;
                      }
                      Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage, me.snippets.growl);
                  }
              }
          });
    },

    /**
     * Called when the user clicked the 'send test mail' button
     */
    onSendTestMail: function() {
        var me = this,
            address = me.getNewsletterEditor().down('textfield').getValue(),
            newsletter = me.createNewsletterModel();

        newsletter.set('status', -1); // mark the newsletter as preview
        newsletter.set('id', null); // don't overwrite existing newsletters

        // The mail-method of the pre-existing newsletter controller reads the newsletters from db.
        // In order not to add additional overhead, simply write the newsletter to db and clean it up next time
        newsletter.save({
            callback: function(data, operation){
                var records = operation.getRecords(),
                    record = records[0],
                    rawData = record.getProxy().getReader().rawData;
                if(operation.success){
                    // do the actual ajax query to send the mail
                    Ext.Ajax.request({
                        url: '{url controller=Newsletter action="mail"}',
                        method: 'POST',
                        params: {
                            id: newsletter.get('id'),
                            testmail: address
                        },
                        success: function(response) {
                            Shopware.Notification.createGrowlMessage(me.snippets.testNewsletter.successTitle, me.snippets.testNewsletter.successMessage, me.snippets.growl);
                        },
                        failure: function(response) {
                            if(rawData && rawData.data) {
                                Shopware.Notification.createGrowlMessage(me.snippets.testNewsletter.errorTitle, me.snippets.testNewsletter.errorMessage + "\r\n<br />" + rawData.data, me.snippets.growl);
                                return;
                            }
                            Shopware.Notification.createGrowlMessage(me.snippets.testNewsletter.errorTitle, me.snippets.testNewsletter.errorMessage, me.snippets.growl);
                        }
                    });
                }else{
                    Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage, me.snippets.growl);
                }
            }
        });
    },

    /**
     * A simple helper function which reads out the settings from the newsletters window.
     * It it used, as the mailing-model cannot be easily written into the settings form as some field-types do not match
     * e.g. dispatch/plaintext or groups
     */
    getSettings: function() {
        var me = this,
            record,
            count, totalCount = 0,
            editor = me.getNewsletterEditor(),
            settingsForm = me.getNewsletterSettings(),
            newsletterGroups = settingsForm.newsletterGroups, customerGroups = settingsForm.customerGroups,
            groups = Ext.create('Shopware.apps.NewsletterManager.store.RecipientGroup'),
            content = editor.tinyMce.getEditor().getContent();

        // Iterate the checkboxes and populate the RecipientGroupStore with checked customer groups
        Ext.each(customerGroups, function(checkbox) {
            var record = checkbox.record,
                count = checkbox.count,
                value = checkbox.getValue();

            //todo@dn: set count to the number of users in the given group
            if(value === true) {
                totalCount += count;

                record = Ext.create('Shopware.apps.NewsletterManager.model.RecipientGroup', {
                    internalId: record.get('id'),
                    number: count,
                    name: record.get('name'),
                    groupkey: record.get('key'),
                    isCustomerGroup: true
                });
                groups.add(record);
            }
        });

        // Iterate the checkboxes and populate the RecipientGroupStore with checked newsletter groups
        Ext.each(newsletterGroups, function(checkbox) {
            var record = checkbox.record,
                    count = checkbox.count,
                value = checkbox.getValue();

            //todo@dn: set count to the number of users in the given group
            if(value === true) {
                totalCount += count;

                record = Ext.create('Shopware.apps.NewsletterManager.model.RecipientGroup', {
                    internalId: record.get('id'),
                    number: count,
                    name: record.get('name'),
                    groupkey: false,
                    isCustomerGroup: false
                });
                groups.add(record);
            }

        });

        var settings = Ext.create('Shopware.apps.NewsletterManager.model.Settings'),
            values = settingsForm.getValues();

        // Copy the values from the form into the settings model
        settings.set(values);

        if(settingsForm.record.get('id') !== null){
            settings.set('id', settingsForm.record.get('id'));
        }
        settings.set('content', content);
        settings.set('groups', groups);
        settings.set('recipients', totalCount);
        if(values['dispatch'] == 1){
            settings.set('plaintext', false);
        }else{
            settings.set('plaintext', true);
        }
        var senderRecord = me.subApplication.senderStore.getById(values['senderId']);
        settings.set('senderName', senderRecord.get('name'));
        settings.set('senderMail', senderRecord.get('email'));

        return settings;
    },

    /**
     * Helper function that creates and returns a newsletter model from the current fields
     * @param record, optional. If given, the given record will be updated instead of creating a new
     */
    createNewsletterModel: function(record) {
        var me = this,
            newsletter, container, ctText, config,
            textStore, containerStore,
            settings = me.getSettings();

        // Create model and store for the text-field (content)
        ctText = Ext.create('Shopware.apps.NewsletterManager.model.ContainerTypeText', {
            headline: settings.get('subject'),
            content:settings.get('content'),
            image: '',
            link: '',
            alignment: 'left'
        });
        textStore = Ext.create('Ext.data.Store', { model: 'Shopware.apps.NewsletterManager.model.ContainerTypeText' });
        textStore.add(ctText);

        // Create model and store for the containers.
        container = Ext.create('Shopware.apps.NewsletterManager.model.Container', {
            value: '',
            type: 'ctText',
            description: '',
            position: 1
        });
        container.getTextStore = textStore;
        containerStore = Ext.create('Ext.data.Store', { model: 'Shopware.apps.NewsletterManager.model.Container' });
        containerStore.add(container);

        config =  {
            subject: settings.get('subject'),
            customerGroup: settings.get('customerGroup'),
            recipients: settings.get('recipients'),
            plaintext: settings.get('plaintext'),
            senderName: settings.get('senderName'),
            senderMail: settings.get('senderMail'),
            groups: settings.get('groups'),
            languageId: settings.get('languageId'),

            date :  null,
            locked: null

        };

        // Create model for the newsletter
        if(record == null) {
            newsletter = Ext.create('Shopware.apps.NewsletterManager.model.Mailing', config);
        }else{
            record.set(config);
            newsletter = record;
        }

        if(settings.get('id') !== null){
            newsletter.set('id', settings.get('id'));
        }

        newsletter.getContainersStore = containerStore;
        newsletter.getGroupsStore = settings.get('groups');

        return newsletter;
    },

    /**
     * Called after the user clicked the 'create preview' button in the newsletter window
     * @param tinyMCE
     */
    onOpenPreview: function(tinyMCE) {
        var me = this,
            newsletter = me.createNewsletterModel();

        newsletter.set('status', -1); // mark the newsletter as preview
        newsletter.set('id', null); // don't overwrite existing newsletters

        // The mail-method of the pre-existing newsletter controller reads the newsletters from db.
        // In order not to add additional overhead, simply write the newsletter to db and clean it up next time
        newsletter.save({
            callback: function(data, operation){
                var records = operation.getRecords(),
                    record = records[0],
                    rawData = record.getProxy().getReader().rawData;
                if(operation.success){
                    if(!Ext.isEmpty(rawData.data)){
                        // Open up a iframe and show the preview
                        var pos = location.href.search("/backend");
                        var url = location.href.substr(0, pos) + "/backend/Newsletter/view?id=" + rawData.data.id;

                        new Ext.Window({
                            title : '{s name=preview}Preview: {/s}' + newsletter.get('subject'),
                            width : 600,
                            height: 480,
                            layout : 'fit',
                            items : [{
                                xtype : "component",
                                id    : 'iframe-win',  // Add id
                                autoEl : {
                                    tag : "iframe",
                                    src : url
                                }
                            }]
                        }).show();

                    }

                }else{
                    if(rawData && rawData.data) {
                        Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage + "\r\n<br />" + rawData.data, me.snippets.growl);
                        return;
                    }
                    Shopware.Notification.createGrowlMessage(me.snippets.saveNewsletter.errorTitle, me.snippets.saveNewsletter.errorMessage, me.snippets.growl);
                }
            }
        });

    }

});
//{/block}<|endoftext|>


<commit_before>// jshint ignore: start
import _ from 'lodash';
import { logger, util } from 'appium-support';
import request from 'request-promise';
import { getSummaryByCode } from '../jsonwp-status/status';
import { errors } from '../mjsonwp/errors';

const log = logger.getLogger('JSONWP Proxy');
// TODO: Make this value configurable as a server side capability
const LOG_OBJ_LENGTH = 1024; // MAX LENGTH Logged to file / console

class JWProxy {
  constructor (opts = {}) {
    Object.assign(this, {
      scheme: 'http',
      server: 'localhost',
      port: 4444,
      base: '/wd/hub',
      sessionId: null
    }, opts);
    this.scheme = this.scheme.toLowerCase();
  }

  // abstract the call behind a member function
  // so that we can mock it in tests
  async request (...args) {
    return await request(...args);
  }

  endpointRequiresSessionId (endpoint) {
    return !_.includes(['/session', '/sessions', '/status'], endpoint);
  }

  getUrlForProxy (url) {
    if (url === '') {
      url = '/';
    }
    const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`;
    const endpointRe = '(/(session|status))';
    let remainingUrl = '';
    if (/^http/.test(url)) {
      const first = (new RegExp(`(https?://.+)${endpointRe}`)).exec(url);
      if (!first) {
        throw new Error('Got a complete url but could not extract JWP endpoint');
      }
      remainingUrl = url.replace(first[1], '');
    } else if ((new RegExp('^/')).test(url)) {
      remainingUrl = url;
    } else {
      throw new Error(`Did not know what to do with url '${url}'`);
    }

    const stripPrefixRe = new RegExp('^.+(/(session|status).*)$');
    if (stripPrefixRe.test(remainingUrl)) {
      remainingUrl = stripPrefixRe.exec(remainingUrl)[1];
    }

    if (!(new RegExp(endpointRe)).test(remainingUrl)) {
      remainingUrl = `/session/${this.sessionId}${remainingUrl}`;
    }

    const requiresSessionId = this.endpointRequiresSessionId(remainingUrl);

    if (requiresSessionId && this.sessionId === null) {
      throw new Error('Trying to proxy a session command without session id');
    }

    const sessionBaseRe = new RegExp('^/session/([^/]+)');
    if (sessionBaseRe.test(remainingUrl)) {
      // we have something like /session/:id/foobar, so we need to replace
      // the session id
      const match = sessionBaseRe.exec(remainingUrl);
      remainingUrl = remainingUrl.replace(match[1], this.sessionId);
    } else if (requiresSessionId) {
      throw new Error(`Could not find :session section for url: ${remainingUrl}`);
    }
    remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes
    return proxyBase + remainingUrl;
  }

  async proxy (url, method, body = null) {
    method = method.toUpperCase();
    const newUrl = this.getUrlForProxy(url);
    const reqOpts = {
      url: newUrl,
      method,
      headers: {
        'Content-type': 'application/json',
        'user-agent': 'appium',
        accept: '*/*',
      },
      resolveWithFullResponse: true,
      timeout: 240000,
      forever: true,
    };
    if (body !== null) {
      if (typeof body !== 'object') {
        body = JSON.parse(body);
      }
      reqOpts.json = body;
    }

    // GET methods shouldn't have any body. Most servers are OK with this, but WebDriverAgent throws 400 errors
    if (method === 'GET') {
      reqOpts.json = null;
    }

    log.debug(`Proxying [${method} ${url || "/"}] to [${method} ${newUrl}] ` +
             (body ? `with body: ${_.truncate(JSON.stringify(body), {length: LOG_OBJ_LENGTH})}` : 'with no body'));

    let res, resBody;
    try {
      res = await this.request(reqOpts);
      resBody = res.body;
      log.debug(`Got response with status ${res.statusCode}: ${_.truncate(JSON.stringify(resBody), {length: LOG_OBJ_LENGTH})}`);
      if (/\/session$/.test(url) && method === 'POST') {
        if (res.statusCode === 200) {
          this.sessionId = resBody.sessionId;
        } else if (res.statusCode === 303) {
          this.sessionId = /\/session\/([^\/]+)/.exec(resBody)[1];
        }
      }
    } catch (e) {
      throw new errors.ProxyRequestError(`Could not proxy command to remote server. `  +
                        `Original error: ${e.message}`, e.error);
    }
    return [res, resBody];
  }

  async command (url, method, body = null) {
    let [response, resBody] = await this.proxy(url, method, body);
    let statusCodesWithRes = [100, 200, 500];
    resBody = util.safeJsonParse(resBody);
    if (_.includes(statusCodesWithRes, response.statusCode) &&
        (_.isUndefined(resBody.status) || _.isUndefined(resBody.value))) {
      throw new Error(`Did not get a valid response object. Object was: ${JSON.stringify(resBody)}`);
    }
    if (_.includes(statusCodesWithRes, response.statusCode)) {
      if (response.statusCode === 200 && resBody.status === 0) {
        return resBody.value;
      } else if (response.statusCode === 200) {
        return resBody;
      }
      let message = getSummaryByCode(resBody.status);
      if (resBody.value.message) {
        message += ` (Original error: ${resBody.value.message})`;
      }
      let e = new Error(message);
      e.status = resBody.status;
      e.value = resBody.value;
      e.httpCode = response.statusCode;
      throw e;
    }
    throw new Error(`Didn't know what to do with response code '${response.statusCode}'`);
  }

  getSessionIdFromUrl (url) {
    const match = url.match(/\/session\/([^\/]+)/);
    return match ? match[1] : null;
  }

  async proxyReqRes (req, res) {
    let [response, body] = await this.proxy(req.originalUrl, req.method, req.body);
    res.headers = response.headers;
    res.set('Content-type', response.headers['content-type']);
    // if the proxied response contains a sessionId that the downstream
    // driver has generated, we don't want to return that to the client.
    // Instead, return the id from the request or from current session
    body = util.safeJsonParse(body);
    if (body && body.sessionId) {
      const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
      if (reqSessionId) {
        log.info(`Replacing sessionId ${body.sessionId} with ${reqSessionId}`);
        body.sessionId = reqSessionId;
      } else if (this.sessionId) {
        log.info(`Replacing sessionId ${body.sessionId} with ${this.sessionId}`);
        body.sessionId = this.sessionId;
      }
    }
    res.status(response.statusCode).send(JSON.stringify(body));
  }
}

export default JWProxy;
<commit_msg>Add configurable request timeout to proxy<commit_after>// jshint ignore: start
import _ from 'lodash';
import { logger, util } from 'appium-support';
import request from 'request-promise';
import { getSummaryByCode } from '../jsonwp-status/status';
import { errors } from '../mjsonwp/errors';

const log = logger.getLogger('JSONWP Proxy');
// TODO: Make this value configurable as a server side capability
const LOG_OBJ_LENGTH = 1024; // MAX LENGTH Logged to file / console
const REQUEST_TIMEOUT = 240000;

class JWProxy {
  constructor (opts = {}) {
    Object.assign(this, {
      scheme: 'http',
      server: 'localhost',
      port: 4444,
      base: '/wd/hub',
      sessionId: null,
      timeout: REQUEST_TIMEOUT,
    }, opts);
    this.scheme = this.scheme.toLowerCase();
  }

  // abstract the call behind a member function
  // so that we can mock it in tests
  async request (...args) {
    return await request(...args);
  }

  endpointRequiresSessionId (endpoint) {
    return !_.includes(['/session', '/sessions', '/status'], endpoint);
  }

  getUrlForProxy (url) {
    if (url === '') {
      url = '/';
    }
    const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`;
    const endpointRe = '(/(session|status))';
    let remainingUrl = '';
    if (/^http/.test(url)) {
      const first = (new RegExp(`(https?://.+)${endpointRe}`)).exec(url);
      if (!first) {
        throw new Error('Got a complete url but could not extract JWP endpoint');
      }
      remainingUrl = url.replace(first[1], '');
    } else if ((new RegExp('^/')).test(url)) {
      remainingUrl = url;
    } else {
      throw new Error(`Did not know what to do with url '${url}'`);
    }

    const stripPrefixRe = new RegExp('^.+(/(session|status).*)$');
    if (stripPrefixRe.test(remainingUrl)) {
      remainingUrl = stripPrefixRe.exec(remainingUrl)[1];
    }

    if (!(new RegExp(endpointRe)).test(remainingUrl)) {
      remainingUrl = `/session/${this.sessionId}${remainingUrl}`;
    }

    const requiresSessionId = this.endpointRequiresSessionId(remainingUrl);

    if (requiresSessionId && this.sessionId === null) {
      throw new Error('Trying to proxy a session command without session id');
    }

    const sessionBaseRe = new RegExp('^/session/([^/]+)');
    if (sessionBaseRe.test(remainingUrl)) {
      // we have something like /session/:id/foobar, so we need to replace
      // the session id
      const match = sessionBaseRe.exec(remainingUrl);
      remainingUrl = remainingUrl.replace(match[1], this.sessionId);
    } else if (requiresSessionId) {
      throw new Error(`Could not find :session section for url: ${remainingUrl}`);
    }
    remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes
    return proxyBase + remainingUrl;
  }

  async proxy (url, method, body = null) {
    method = method.toUpperCase();
    const newUrl = this.getUrlForProxy(url);
    const reqOpts = {
      url: newUrl,
      method,
      headers: {
        'Content-type': 'application/json',
        'user-agent': 'appium',
        accept: '*/*',
      },
      resolveWithFullResponse: true,
      timeout: this.timeout,
      forever: true,
    };
    if (body !== null) {
      if (typeof body !== 'object') {
        body = JSON.parse(body);
      }
      reqOpts.json = body;
    }

    // GET methods shouldn't have any body. Most servers are OK with this, but WebDriverAgent throws 400 errors
    if (method === 'GET') {
      reqOpts.json = null;
    }

    log.debug(`Proxying [${method} ${url || "/"}] to [${method} ${newUrl}] ` +
             (body ? `with body: ${_.truncate(JSON.stringify(body), {length: LOG_OBJ_LENGTH})}` : 'with no body'));

    let res, resBody;
    try {
      res = await this.request(reqOpts);
      resBody = res.body;
      log.debug(`Got response with status ${res.statusCode}: ${_.truncate(JSON.stringify(resBody), {length: LOG_OBJ_LENGTH})}`);
      if (/\/session$/.test(url) && method === 'POST') {
        if (res.statusCode === 200) {
          this.sessionId = resBody.sessionId;
        } else if (res.statusCode === 303) {
          this.sessionId = /\/session\/([^\/]+)/.exec(resBody)[1];
        }
      }
    } catch (e) {
      throw new errors.ProxyRequestError(`Could not proxy command to remote server. `  +
                        `Original error: ${e.message}`, e.error);
    }
    return [res, resBody];
  }

  async command (url, method, body = null) {
    let [response, resBody] = await this.proxy(url, method, body);
    let statusCodesWithRes = [100, 200, 500];
    resBody = util.safeJsonParse(resBody);
    if (_.includes(statusCodesWithRes, response.statusCode) &&
        (_.isUndefined(resBody.status) || _.isUndefined(resBody.value))) {
      throw new Error(`Did not get a valid response object. Object was: ${JSON.stringify(resBody)}`);
    }
    if (_.includes(statusCodesWithRes, response.statusCode)) {
      if (response.statusCode === 200 && resBody.status === 0) {
        return resBody.value;
      } else if (response.statusCode === 200) {
        return resBody;
      }
      let message = getSummaryByCode(resBody.status);
      if (resBody.value.message) {
        message += ` (Original error: ${resBody.value.message})`;
      }
      let e = new Error(message);
      e.status = resBody.status;
      e.value = resBody.value;
      e.httpCode = response.statusCode;
      throw e;
    }
    throw new Error(`Didn't know what to do with response code '${response.statusCode}'`);
  }

  getSessionIdFromUrl (url) {
    const match = url.match(/\/session\/([^\/]+)/);
    return match ? match[1] : null;
  }

  async proxyReqRes (req, res) {
    let [response, body] = await this.proxy(req.originalUrl, req.method, req.body);
    res.headers = response.headers;
    res.set('Content-type', response.headers['content-type']);
    // if the proxied response contains a sessionId that the downstream
    // driver has generated, we don't want to return that to the client.
    // Instead, return the id from the request or from current session
    body = util.safeJsonParse(body);
    if (body && body.sessionId) {
      const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
      if (reqSessionId) {
        log.info(`Replacing sessionId ${body.sessionId} with ${reqSessionId}`);
        body.sessionId = reqSessionId;
      } else if (this.sessionId) {
        log.info(`Replacing sessionId ${body.sessionId} with ${this.sessionId}`);
        body.sessionId = this.sessionId;
      }
    }
    res.status(response.statusCode).send(JSON.stringify(body));
  }
}

export default JWProxy;
<|endoftext|>